Compare commits

...

225 Commits

Author SHA1 Message Date
EricaLiu
020b5f3984 Add security schema for nacos mcp (#2847) 2025-09-01 15:23:04 +08:00
澄潭
9a12f0b593 rel: Release v2.1.7 (#2849) 2025-09-01 15:22:17 +08:00
韩贤涛
7e74eeb333 feat(wasm-plugin): add tests and docs for hmac-auth-apisix (#2842) 2025-09-01 14:07:57 +08:00
澄潭
fff5903007 feat: add MCP SSE stateful session load balancer support (#2818) 2025-08-28 20:52:07 +08:00
Jingze
a00b810be5 feat(wasm-go): add wasm go plugin unit test and ci workflow (#2809) 2025-08-28 20:02:03 +08:00
澄潭
3e0a5f02a7 feat(wasm-plugin): add jsonrpc-converter plugin (#2805) 2025-08-28 19:28:37 +08:00
澄潭
44c33617fa feat(ai-proxy): add OpenRouter provider support (#2823) 2025-08-28 19:26:21 +08:00
韩贤涛
b2ffeff7b8 feat(wasm-plugin): add hmac-auth-apisix plugin (#2815) 2025-08-26 21:10:43 +08:00
Asnowww
c0ddbccbfe chore: fix typos (#2816) 2025-08-26 14:41:05 +08:00
澄潭
16a18c6609 feat(ai-proxy): add auto protocol compatibility for OpenAI and Claude APIs (#2810) 2025-08-25 14:13:51 +08:00
Xijun Dai
72b98ab6cf feat(ai-proxy): add anthropic/v1/messages and openai/v1/models support for DeepSeek (#2808)
Signed-off-by: Xijun Dai <daixijun1990@gmail.com>
2025-08-21 17:40:13 +08:00
xingpiaoliang
df20472f7b fix(wasm-go-build): correct the build command (#2799) 2025-08-21 09:47:30 +08:00
co63oc
9186b5505d chore: fix typos (#2770) 2025-08-20 10:38:03 +08:00
co63oc
eaea782693 fix RegisteTickFunc (#2787) 2025-08-19 20:53:53 +08:00
rinfx
890a802481 feat(ai-proxy): bedrock support tool use (#2730) 2025-08-19 16:54:50 +08:00
github-actions[bot]
bb69a1d50b Update CRD file in the helm folder (#2769)
Co-authored-by: CH3CHO <2909796+CH3CHO@users.noreply.github.com>
2025-08-19 16:54:27 +08:00
zat366
5a023512fa feat(mcp-server): update the dependency github.com/higress-group/wasm-go to support MCP response images (#2788) 2025-08-19 16:52:14 +08:00
Kent Dong
47f0478ef5 fix: Remove "accept-encoding" header for mcp-sse upstreams (#2786) 2025-08-19 15:53:49 +08:00
Kent Dong
c9fa8d15db chore: Restructure the path-to-api-name mapping logic in ai-proxy (#2773) 2025-08-18 19:05:23 +08:00
Kent Dong
0f1afcdcca fix(ai-proxy): Do not change the configured components of Azure URL (#2782) 2025-08-18 16:27:25 +08:00
StarryNight
19d1548971 update ai-prompt-decorator to new plugin wrapper api (#2777) 2025-08-18 11:01:35 +08:00
Kent Dong
24dca0455e fix: Fix bugs in the bedrock model name escaping logic (#2663)
Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
2025-08-15 17:40:13 +08:00
澄潭
be603af461 Update CODEOWNERS 2025-08-15 16:53:31 +08:00
澄潭
8796c6040f Update CODEOWNERS 2025-08-15 16:52:45 +08:00
Kent Dong
15edc79fb3 fix: Fix the malfunction of _match_service_ rules in C++ Wasm plugins (#2723) 2025-08-15 16:47:30 +08:00
Kent Dong
5822868f87 feat: Support adding a proxy server in between when forwarding requests to upstream (#2710)
Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
2025-08-15 16:15:42 +08:00
澄潭
995bcc2168 feat(transformer): Add split and retain strategy for dedupe (#2761) 2025-08-15 15:21:13 +08:00
Kent Dong
a3310f1a3b fix: Allow duplicated items in the IP list of ip-restriction config (#2755)
Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
2025-08-15 13:39:26 +08:00
Jingze
0bb934073a fix(golang-filter): fix mcp server contruct envoy filter unit test (#2757) 2025-08-13 17:43:15 +08:00
Jingze
247de6a349 fix(golang-filter): fix bug of stop and buffer in decode data (#2754) 2025-08-13 11:37:01 +08:00
co63oc
79b3b23aab Fix typos (#2628)
Signed-off-by: co63oc <co63oc@users.noreply.github.com>
Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
Co-authored-by: xingpiaoliang <erasernoobx@outlook.com>
2025-08-12 15:58:32 +08:00
Jingze
b9d6343efa fix(ip-restriction): fix bug of set ip_source_type (#2743) 2025-08-11 18:47:48 +08:00
xingpiaoliang
0af00bef6b feat(ai-proxy): gemini model multimodal support (#2698) 2025-08-11 15:54:34 +08:00
Kent Dong
953b95cf92 chore: Update some log levels in ai-statistics (#2740) 2025-08-11 13:59:01 +08:00
aias00
a76808171f feat: improve ai statistic plugin (#2671) 2025-08-11 13:43:00 +08:00
rinfx
f7813df1d7 add value length limit for ai statistics, truncate when over limit (#2729) 2025-08-11 11:12:03 +08:00
WeixinX
33ce18df5a feat(wasm-go): add field reroute to disable route reselection (#2739) 2025-08-11 09:37:35 +08:00
aias00
a1bf1ff009 feat(provider): add support for Grok provider in AI proxy (#2713)
Co-authored-by: 韩贤涛 <601803023@qq.com>
2025-08-07 17:22:47 +08:00
澄潭
b69e3a8f30 Deprecate the use of slash as a concatenation character for mcp server and tool, to avoid non-compliance with function naming conventions. (#2711) 2025-08-07 15:18:31 +08:00
NOBODY
5ee878198c feat(ai-proxy): gemini model thinking support (#2712) 2025-08-06 06:50:46 +08:00
rinfx
943fda0a9c AI security streaming (#2696) 2025-08-04 20:47:18 +08:00
WeixinX
abc31169a2 fix(wasm-go): transformer performs an add op when the replace key does not exist (#2706) 2025-08-04 20:38:29 +08:00
韩贤涛
5f65b4f5b0 feat: Rust WASM supports Redis database configuration option (#2704) 2025-08-03 12:56:27 +08:00
澄潭
645646fe22 Fix the issue where AI route fallback does not work when using Bedrock. (#2653) 2025-07-31 20:16:16 +08:00
澄潭
4acb65cc67 Update README_ZH.md 2025-07-31 14:26:07 +08:00
github-actions[bot]
e63a2e0251 Add release notes (#2693)
Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
2025-07-31 14:23:54 +08:00
澄潭
d98f8b8b21 release v2.1.6 (#2688) 2025-07-30 21:52:51 +08:00
aias00
bd19a5049b feat(rust): improve rust readme (#2668) 2025-07-30 21:22:50 +08:00
aias00
1070541f1d fix(doc): fix some dead link (#2675) 2025-07-30 21:22:15 +08:00
lvshui
32b5c89c17 fix: postgres to mcp-server tools: describeTable fail (#2687) 2025-07-30 21:18:56 +08:00
shaoyin.zj
bd1101d711 feat(mcp): Get ErrorResponseTemplate from nacos mcp registry (#2650) 2025-07-30 21:18:37 +08:00
xingpiaoliang
27680223b9 feat(ingress/annotation): support external service in the mirror annotations (#2679) 2025-07-30 21:15:35 +08:00
hongzhouzi
93ea5e7355 fix: Error compiling golang-filter.so on arm64 machine (#2494) (#2507)
Signed-off-by: hongzhouzi <weihongzhou.whz@alibaba-inc.com>
2025-07-30 13:42:31 +08:00
澄潭
ff9a29c5d9 opt: mcp endpoint parser improvements (#2673) 2025-07-29 12:41:29 +08:00
韩贤涛
6a1557f6ac feat: ai-token-ratelimit support setting global rate limit thresholds for routes​ (#2667) 2025-07-28 08:14:35 +08:00
johnlanni
e6e4193679 update envoy commit 2025-07-25 20:54:28 +08:00
澄潭
978d0afb63 envoy: fix proxy-wasm memleak&ppv2 port mapping (#2662) 2025-07-25 20:24:41 +08:00
澄潭
39dd4538c9 opt:relax dns service domain validation (#2661) 2025-07-25 20:15:54 +08:00
co63oc
f826d79109 fix typos (#2656)
Signed-off-by: co63oc <co63oc@users.noreply.github.com>
2025-07-25 16:18:39 +08:00
Kent Dong
7348c265b5 feat: Support model mapping and more URL configuration formats for Azure OpenAI (#2649) 2025-07-25 11:28:02 +08:00
OxalisCu
ea0bf7c1b7 feat(ai-proxy): support first byte timeout for llm streaming requests (#2652)
Signed-off-by: OxalisCu <2127298698@qq.com>
2025-07-23 21:49:42 +08:00
Xijun Dai
ba1bf353b8 feat(ai-proxy): add anthropic/v1/messages support for qwen (#2648)
Signed-off-by: Xijun Dai <daixijun1990@gmail.com>
2025-07-23 21:06:56 +08:00
澄潭
b56097e647 Update CODEOWNERS 2025-07-23 19:24:13 +08:00
GuoChenxu
5b97b849b5 Release note format (#2647)
Signed-off-by: guochenxu <guochenxu11@outlook.com>
2025-07-23 19:07:53 +08:00
github-actions[bot]
331fe57c70 Add release notes (#2635) 2025-07-22 14:48:45 +08:00
澄潭
4d32cc9468 Disable reroute in some plugins (#2639) 2025-07-22 14:44:26 +08:00
GuoChenxu
34b5a6feea Fix the special character translation issue in pr #2596 (#2623)
Signed-off-by: guochenxu <guochenxu11@outlook.com>
2025-07-21 18:54:08 +08:00
GuoChenxu
8736edaf61 CI: 基于higress-report-agent实现higress release note agent (#2596)
Signed-off-by: guochenxu <guochenxu11@outlook.com>
2025-07-18 17:53:58 +08:00
澄潭
30d5b4d32e upgrade wasm-go sdk of some wasm plugins (#2615) 2025-07-17 21:03:42 +08:00
Alexander Kolotov
c0133378a7 Add the Blockscout MCP server (#2585) 2025-07-17 09:42:28 +08:00
Xijun Dai
8346b4a4a2 feat: Support statistics images/audio/responses API Token usage (#2542)
Signed-off-by: Xijun Dai <daixijun1990@gmail.com>
2025-07-16 10:34:09 +08:00
xingpiaoliang
ce271849de docs(wasm-go): update README related to wasm-go (#2586)
Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
2025-07-16 10:27:40 +08:00
澄潭
bdc3ecab71 update go to 1.24.4 in wasm builder image (#2600) 2025-07-16 10:24:29 +08:00
澄潭
9214dca078 update go to 1.24.4 in wasm builder image (#2598) 2025-07-15 19:36:04 +08:00
Xijun Dai
c3eb8d0447 feat(ai-proxy): add anthropic && gemini apiName (#2551)
Signed-off-by: Xijun Dai <daixijun1990@gmail.com>
2025-07-15 19:15:07 +08:00
xingpiaoliang
081ab6ee8d Migrate WASM Go Plugins to New SDK and Go 1.24 (#2532) 2025-07-11 10:43:00 +08:00
rinfx
9a45f07972 fix: [ai-load-balancer]move the logic of request count to HttpStreamDone phase (#2564) 2025-07-09 15:42:00 +08:00
mamba
da2ae4c7ee 增加 useManifestAsEntry 配置支持 || Increase the useManifestAsEntry configuration support (#2499)
Co-authored-by: rinfx <yucheng.lxr@alibaba-inc.com>
2025-07-09 11:53:56 +08:00
woody
ff068258a1 [ai-proxy]qwen text-rerank support (#2537) 2025-07-07 20:27:56 +08:00
woody
0996ad21b1 feat(ai-proxy): add basePathHandling option (#2535)
Co-authored-by: Se7en <chengzw258@163.com>
2025-07-03 20:34:14 +08:00
Se7en
45eb76d4cc feat: Add Higress API MCP server (#2517) 2025-07-03 10:27:28 +08:00
澄潭
36bcb595d6 Release 2.1.5 (#2536) 2025-07-02 18:03:46 +08:00
hongzhouzi
783a8db512 feat: add DB MCP Server execute, list tables, describe table tools (#2506)
Signed-off-by: hongzhouzi <weihongzhou.whz@alibaba-inc.com>
2025-07-02 14:47:49 +08:00
澄潭
44566f5259 feat: ai-proxy support config subPath field (#2533) 2025-07-02 11:35:28 +08:00
Xijun Dai
73ba9238bd feat(helm): comment tracing.skywalking (#2514)
Signed-off-by: Xijun Dai <daixijun1990@gmail.com>
Co-authored-by: Se7en <chengzw258@163.com>
2025-07-01 22:09:33 +08:00
Xijun Dai
41a1455874 fix(ai-proxy): restrict the stream_options parameter to be effective only in the openai/v1/chatcompletions (#2524)
Signed-off-by: Xijun Dai <daixijun1990@gmail.com>
2025-07-01 21:44:34 +08:00
rinfx
9d68ccbf35 feat: advanced load balance policys for LLM service through wasm plugin (#2531) 2025-07-01 20:08:44 +08:00
Kent Dong
db7dbb24a2 fix: Fix an incorrect config property name in the README of ai-proxy plugin (#2503) 2025-07-01 16:29:18 +08:00
HaoJie Liu
9a0cf9b762 fix(ai-proxy): add system message handling for Bedrock requests (#2516) 2025-06-30 10:35:14 +08:00
Xijun Dai
bb786c9618 feat(ai-proxy): add responses support for doubao (#2509)
Signed-off-by: Xijun Dai <daixijun1990@gmail.com>
2025-06-28 10:39:21 +08:00
johnlanni
ef49d2f5f6 fix url schema issue 2025-06-26 15:51:06 +08:00
HaoJie Liu
864bf5af39 fix(ai-proxy): bedrock support additional request fields (#2480) 2025-06-26 11:29:32 +08:00
澄潭
527e922d50 Fix the incorrect behavior of decoding when constructing and sending requests if the path in the configured URL contains URL-encoded parts. (#2497) 2025-06-26 11:22:38 +08:00
kai2321
1fe5eb6e13 Implement AI-image-reader plugin (#1925) 2025-06-25 19:28:02 +08:00
澄潭
87185baff2 Update CODEOWNERS 2025-06-25 13:41:22 +08:00
rinfx
76ada0b844 add trace_span_key & as_seperate_log_field configuration for ai-statistics (#2488) 2025-06-25 09:28:14 +08:00
澄潭
f4d3fec228 feat: mcp server support error template response (#2485) 2025-06-24 11:05:54 +08:00
Xijun Dai
e94ac43dd1 fix(ai-proxy): fix openai provider customPath compatibility (#2475)
Signed-off-by: Xijun Dai <daixijun1990@gmail.com>
2025-06-21 08:23:02 +08:00
Jacky Wu
dd29267fd7 fix: add missing controller sa annotation. (#2443) 2025-06-20 16:36:10 +08:00
woody
01a9161153 fix(ai-proxy): Unify the naming convention & fix api name mapping (#2441) 2025-06-20 16:35:30 +08:00
Kenneth
ceb8b557dc feat: add investoday MCP Server (#2450)
Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
2025-06-20 15:58:28 +08:00
007gzs
753022e093 Feat: Ai data masking msg window support reasoning_content in response and n in request (#2404) 2025-06-20 15:39:09 +08:00
xingpiaoliang
04cbbfc7e8 feat(mcp/sse): support passthourgh the query parameter in sse server to the rest api server (#2460) 2025-06-20 15:07:45 +08:00
Xin Luo
db66df39c4 fix too much logs when nacos is not avaiable (#2469) 2025-06-20 14:36:01 +08:00
澄潭
dad6278a6d refactor: mcp server depends on the latest wasm-go repository (#2458) 2025-06-18 20:32:47 +08:00
johnlanni
272d693df3 fix higress-console version in helm chart 2025-06-18 09:15:46 +08:00
澄潭
69bc800198 fix: The mcp to rest capability of the mcp server supports returning status without returning a body from the backend, and instead responds via sse (#2445) 2025-06-17 21:26:38 +08:00
澄潭
1daaa4b880 release 2.1.5-rc.1 (#2446) 2025-06-17 21:23:42 +08:00
澄潭
6e31a7b67c update envoy and istio (#2440) 2025-06-17 17:22:46 +08:00
澄潭
91f070906a feat: add mcp-router plugin (#2409) 2025-06-17 15:40:13 +08:00
澄潭
e3aeddcc24 add release-notes of 2.1.4 (#2433) 2025-06-17 14:41:14 +08:00
woody
926913f0e7 feat(ai-proxy): add support for OpenAI Fine-Tuning API (#2424) 2025-06-17 13:44:00 +08:00
mirror
c471bb2003 feat: add default route support for wanx image&video synthesis (#2431) 2025-06-17 13:43:26 +08:00
澄潭
0b9256617e fix: When configuring an MCP server for SSE forwarding, the controller may crash (#2423) 2025-06-16 16:08:39 +08:00
hourmoneys
2670ecbf8e feat: Add AI-based bidding information tool MCP service (#2343) 2025-06-16 10:14:46 +08:00
mirror
7040e4bd34 feat: support for wanxiang image/video generation in ai-proxy & ai-statistics (#2378) 2025-06-16 09:39:37 +08:00
xuruidong
de8a4d0b03 docs: fix broken link in mcp-servers README_zh.md (#2418) 2025-06-15 22:14:10 +08:00
Xijun Dai
b33a3a4d2e fix(ai-proxy): fix gemini provider missing finishReason (#2408)
Signed-off-by: Xijun Dai <daixijun1990@gmail.com>
Co-authored-by: Se7en <chengzw258@163.com>
2025-06-13 21:51:44 +08:00
澄潭
087cb48fc5 opt: unify the end-of-line markers in the MCP session filter. (#2403) 2025-06-12 18:58:56 +08:00
hourmoneys
95f32002d2 add mcp-server doc (#2327) 2025-06-12 17:14:39 +08:00
Xijun Dai
fb8dd819e9 feat(ai-proxy): Adjust the streaming response structure to keep it consistent with the openai (#2391)
Signed-off-by: Xijun Dai <daixijun1990@gmail.com>
2025-06-12 16:25:35 +08:00
EricaLiu
86934b3203 fix: fix const McpStreamableProtocol spell mistake (#2405) 2025-06-12 15:35:39 +08:00
HaoJie Liu
38068ee43d fix(ai-proxy): fix bedrock Sigv4 mismatch (#2402) 2025-06-12 10:46:02 +08:00
EricaLiu
d81573e0d2 fix: change auto generate se namespace to mcp (#2398) 2025-06-11 20:30:48 +08:00
tangchang
312b80f91d feat: Plugin server supports k8s deployment and configures the default download URL of the plugin(#2232, #2280,#2312) (#2389)
Co-authored-by: xujingfeng <jingfeng.xjf@alibaba-inc.com>
Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
2025-06-11 12:20:09 +08:00
zty98751
e42e6eeee6 split translae-readme from helm-docs action 2025-06-11 09:52:41 +08:00
澄潭
9f5067d22f Update release-hgctl.yaml 2025-06-10 22:21:42 +08:00
澄潭
6af9587372 Update release-crd.yaml 2025-06-10 22:21:00 +08:00
johnlanni
5812c1e734 release 2.1.4 2025-06-10 20:58:22 +08:00
github-actions[bot]
bafbe7972d Update CRD file in the helm folder (#2392)
Co-authored-by: CH3CHO <2909796+CH3CHO@users.noreply.github.com>
2025-06-10 20:29:23 +08:00
Kent Dong
f3fbf7d6c8 fix: Support mixing line breaks in a single SSE response (#2344) 2025-06-10 20:21:04 +08:00
EricaLiu
1666dfb01c fix : fix credential process logic for nacos mcp util and add ut for it (#2394) 2025-06-10 20:03:45 +08:00
EricaLiu
d2f09fe8c5 fix: refactored mcp server auto discovery logic and fix some issue (#2382)
Co-authored-by: johnlanni <zty98751@alibaba-inc.com>
2025-06-10 17:11:34 +08:00
Xijun Dai
69d877c116 feat(ai-proxy): 添加 Claude 图片理解与 Tools 调用能力 || feat(ai-proxy): Add Claude image understanding and Tools calling capabilities (#2385)
Signed-off-by: Xijun Dai <daixijun1990@gmail.com>
2025-06-10 15:11:18 +08:00
澄潭
5bc0058779 add upstream override wasm abi (#2387) 2025-06-10 14:20:02 +08:00
HaoJie Liu
d4e114b152 feat(ai-proxy): support Google Cloud Vertex (#2119)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2025-06-09 18:11:30 +08:00
Xijun Dai
e674c780c6 feat(ai-proxy): add models & image generation support for gemini (#2380)
Signed-off-by: Xijun Dai <daixijun1990@gmail.com>
Co-authored-by: Kent Dong <ch3cho@qq.com>
2025-06-08 15:25:22 +08:00
mamba
26cd6837d5 feat(frontend-gray): Add uniqueGrayTag configuration detection (#2371)
Co-authored-by: rinfx <yucheng.lxr@alibaba-inc.com>
2025-06-07 15:35:28 +08:00
Xijun Dai
5674d91a10 feat(ai-proxy): 修复 openai 配置 openaiCustomUrl 之后, 对不支持 Api 透传路径错误的问题 || feat(ai-proxy): Fixed the issue that the API pass-through path error does not support openaiCustomUrl after openai is configured. (#2364)
Signed-off-by: Xijun Dai <daixijun1990@gmail.com>
2025-06-06 17:02:56 +08:00
澄潭
c78b4aaba3 Update README.md 2025-06-05 13:36:33 +08:00
澄潭
0e4e8da9c1 Update README.md 2025-06-05 13:35:58 +08:00
澄潭
c9ec8a12bb Update README.md 2025-06-05 12:00:59 +08:00
澄潭
7484bcea62 Update README.md 2025-06-05 12:00:08 +08:00
Xijun Dai
896780b60e feat(ai-proxy): add modelMapping regexp support (#2358)
Signed-off-by: Xijun Dai <daixijun1990@gmail.com>
2025-06-03 22:29:17 +08:00
澄潭
7b1ae49cd4 fix content-length header not remove in ai-search plugin (#2363) 2025-06-03 20:40:14 +08:00
VinciWu557
ee26baf054 feat: support dify ai-proxy e2e test || feat: support diify ai-proxy e2e test (#2319) 2025-06-03 19:31:58 +08:00
Xijun Dai
33fc47cefb feat(ai-proxy): add batches & files support (#2355)
Signed-off-by: Xijun Dai <daixijun1990@gmail.com>
2025-06-03 09:42:36 +08:00
澄潭
19946d46ca Update README.md 2025-05-30 17:24:28 +08:00
mirror
52d0212698 fix: set "EnableSemanticCachefalse" to false when no vector configured in ai-cache (#2351) 2025-05-30 13:38:06 +08:00
Xijun Dai
a73c33f1da feat(ai-proxy): support OpenAI-compatible image and audio model Mapping (#2341) 2025-05-30 12:16:52 +08:00
韩贤涛
69b755a10d feat: cluster-key-rate-limit support setting global rate limit thresholds for routes​ (#2262) 2025-05-29 09:57:10 +08:00
johnlanni
52464c0e06 fix empty authority rewrite in mcp-server plugin 2025-05-28 19:56:16 +08:00
澄潭
d7d5d1c571 Update README.md 2025-05-28 15:31:12 +08:00
johnlanni
ea948ee818 add more info log in mcp-server 2025-05-28 10:30:35 +08:00
Xijun Dai
767f51adce feat(ai-proxy): add doubao Image Generation support (#2331)
Signed-off-by: Xijun Dai <daixijun1990@gmail.com>
2025-05-27 18:59:07 +08:00
HaoJie Liu
168cb04c61 fix(ai-proxy): URL encode model name in Bedrock requests (#2321) 2025-05-27 16:06:52 +08:00
johnlanni
323aabf72b rm .tgitconfig 2025-05-27 07:14:13 +08:00
澄潭
b8d75598ed Update mcp-server.yaml 2025-05-26 16:51:03 +08:00
johnlanni
b37649a62f update README of shebao-tools mcp server 2025-05-26 16:31:14 +08:00
澄潭
76f76a70ab add info log of ai-search plugin (#2323) 2025-05-26 16:23:59 +08:00
澄潭
647c961f51 Update README.md 2025-05-26 16:12:52 +08:00
澄潭
5a5a72a9f8 Update README.md 2025-05-26 16:09:30 +08:00
Kent Dong
ffcf5df28a feat: Refactor mcpServer.matchList config generation logic (#2207) 2025-05-26 15:26:44 +08:00
Se7en
ec83623614 feat: allow skipping higress dev image build during wasmplugin e2e tests (#2264) 2025-05-26 10:20:05 +08:00
Kent Dong
bf5be07d74 feat: Add a github action to copy CRD definitions from api folder to helm folder (#2268) 2025-05-26 10:10:56 +08:00
hourmoneys
f6bb5d7729 add mcp service shebao tools (#2303) 2025-05-23 17:27:15 +08:00
Whitea
031ae21caa feat(mcp-server): add HackMD mcp server (#2260) 2025-05-22 16:53:01 +08:00
Forgottener
fa3c5ea0fc feat: Supports recording request header, request body, response header and response body information in the access log (#2265) 2025-05-21 16:15:05 +08:00
澄潭
93436db13c fix proxy-wasm-cpp-sdk (#2281) 2025-05-21 13:59:27 +08:00
xujingfeng
be2c6f8a4a fix: modify log level WARN -> DEBUG in key-auth plugin (#2275) 2025-05-20 13:52:17 +08:00
EricaLiu
c768973e47 Fix : add fail strategy for wasmplugin generated by mcp server (#2237) 2025-05-15 16:28:37 +08:00
澄潭
8ec65ed377 mcp server support API auth through OAS3 security schemes || mcp server support API auth through OAS3 security schemes (#2241) 2025-05-15 15:48:27 +08:00
Rishi Mondal
675a8ce4a9 Add test translation workflow (#2228)
Signed-off-by: Rishi Mondal <mavrickrishi@gmail.com>
2025-05-14 17:35:50 +08:00
澄潭
06c5ddd80b Update README.md 2025-05-14 16:25:00 +08:00
EricaLiu
8ccc170500 fix : fix issue #2222 (#2231) 2025-05-14 15:40:19 +08:00
韩贤涛
ff308d5292 fix: Remove the Authorization request header when using AI-proxy to proxy Gemini (#2220) 2025-05-13 09:36:04 +08:00
littlejian
af8502b0b0 feat: update translate-readme action (#2208) 2025-05-12 14:34:04 +08:00
Kent Dong
c683936b1c fix: Fix the incorrect rewrite config generated for Nacos 3 MCP Servers (#2211) 2025-05-12 14:30:37 +08:00
Xijun Dai
8b3f1aab1a feat(ai-proxy): support Amazon Bedrock Image Generation (#2212)
Signed-off-by: Xijun Dai <daixijun1990@gmail.com>
Co-authored-by: Kent Dong <ch3cho@qq.com>
2025-05-10 09:54:31 +08:00
johnlanni
b5eadcdbee release v2.1.3 2025-05-09 15:30:22 +08:00
EricaLiu
8ca8fd27ab fix param type error (#2204) 2025-05-09 14:55:10 +08:00
Kent Dong
ab014cf912 feat: Add SSE direct proxy support to mcp-session filter (#2157) 2025-05-09 14:28:42 +08:00
EricaLiu
3f67b05fab fix : fix vs rewrite when mcp protocol is http (#2203) 2025-05-09 14:03:31 +08:00
HaoJie Liu
cd271c1f87 fix(ai-statistics): adjust requestBodyBufferLimit (#2192)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2025-05-08 16:18:50 +08:00
johnlanni
755de5ae67 add original path info in mcp-server 2025-05-07 21:17:18 +08:00
johnlanni
40402e7dbd refactor route call in mcp-server 2025-05-07 20:36:41 +08:00
johnlanni
0a2fb35ae2 fix gemini provider in ai-proxy 2025-05-07 16:54:40 +08:00
澄潭
b16954d8c1 Update README.md 2025-05-07 15:27:28 +08:00
Kent Dong
29370b18d7 feat: Support /v1/models API in ai-proxy (#2164) 2025-05-06 15:53:13 +08:00
EricaLiu
c9733d405c fix : Add nacos username and password login option (#2170) 2025-05-06 15:18:45 +08:00
johnlanni
ec6004dd27 update golang filter dependency 2025-04-30 23:33:04 +08:00
Jingze
ea9a6de8c3 fix: update golang filter README (#2147) 2025-04-29 22:08:10 +08:00
github-actions[bot]
5e40a700ae Update helm translated README.zh.md (#2152) 2025-04-29 21:04:23 +08:00
johnlanni
48b220453b release 2.1.2 2025-04-29 20:53:50 +08:00
mirror
489a800868 add: add mcp-context7 descriptions (#2149) 2025-04-29 20:44:00 +08:00
澄潭
60c9f21e1c When the service source type is nacos3, if mcpserver is turned off, then the discovery mechanism of nacos2 will be enabled (#2150) 2025-04-29 17:29:52 +08:00
Jingze
ab73f21017 fix: make mcp server redis client config based (#2145)
Co-authored-by: daijingze_mac <18373118@buaa.edu.cn>
2025-04-29 14:27:48 +08:00
EricaLiu
806563298b fix : when nacos push empty service instance list, should skip generate (#2144) 2025-04-29 11:38:51 +08:00
github-actions[bot]
02fabbb35f Update helm translated README.zh.md (#2141) 2025-04-29 09:28:20 +08:00
johnlanni
07154d1f49 set mcp-go dependency to 0.12 2025-04-28 23:02:28 +08:00
johnlanni
db30c0962a update go mod 2025-04-28 22:04:21 +08:00
johnlanni
731fe43d14 update envoy-release version 2025-04-28 21:59:30 +08:00
EricaLiu
5bd20aa559 feat : support mcp server auto discovery for nacos registry (#2122)
Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
2025-04-28 21:58:17 +08:00
johnlanni
a2e4f944e9 rel 2.1.2-rc.1 2025-04-28 20:39:02 +08:00
johnlanni
7955aec639 change golang-filter build image 2025-04-28 19:43:17 +08:00
johnlanni
e12feb9f57 golang-filter use go 1.22 2025-04-28 19:21:09 +08:00
zty98751
03b4144cff update submodule commit 2025-04-28 17:42:58 +08:00
Jingze
c382635e7f fix: Refactor MCP Server into MCP Session and MCP Server (#2120) 2025-04-28 13:42:14 +08:00
007gzs
e381806ba0 fix: ai_data_masking add compatibility handling for non-compliant API response structures (#2130) 2025-04-27 14:31:02 +08:00
johnlanni
52114b37f8 update mcp server config fields in mcp-bridge api 2025-04-27 11:10:08 +08:00
mirror
b4e68c02f9 add mcp yuque descriptions (#2125) 2025-04-25 18:08:42 +08:00
Tsukilc
c241ccf19d test: add test for /pkg/ingress/kube/common (#2123) 2025-04-24 20:03:57 +08:00
澄潭
e4fa1e6390 Update README_ZH.md 2025-04-24 19:08:40 +08:00
澄潭
b103b9d7cb Update README.md 2025-04-24 19:08:10 +08:00
johnlanni
90b02a90e0 update mcpbridge proto 2025-04-24 15:52:17 +08:00
mirror
38f718b965 update github & e2bdev mcp descriptions (#2107) 2025-04-23 20:08:21 +08:00
johnlanni
8752a763c2 update all-in-one mcp-server 2025-04-23 14:42:44 +08:00
HaoJie Liu
a57173ce28 feat(ai-proxy): support Amazon Bedrock (#2039) 2025-04-22 22:36:14 +08:00
mirror
3a8d8f5b94 update mcp descriptions (#2105) 2025-04-22 17:01:41 +08:00
Kent Dong
1c37c361e1 feat: Support extracting model argument from body in multipart/form-data format (#1940) 2025-04-22 13:52:50 +08:00
Se7en
b8133a95b2 feat: optimize elasticsearch ai-search plugin and update related docs" (#2100) 2025-04-22 13:33:38 +08:00
johnlanni
36d5d391b8 update README.md 2025-04-21 09:59:37 +08:00
johnlanni
1da9a07866 update README 2025-04-21 09:56:23 +08:00
ZeruiYang
8620838f8b fix: update module replacements (#2090) 2025-04-19 18:13:42 +08:00
waTErMo0n
e7d2005382 feat:Getting MatchLabels dynamically via gatewaySelectorKey/Value #1857 (#1883) 2025-04-18 17:46:47 +08:00
676 changed files with 70896 additions and 7145 deletions

View File

@@ -3,22 +3,22 @@ name: Build and Push Wasm Plugin Image
on:
push:
tags:
- "wasm-*-*-v*.*.*" # 匹配 wasm-{go|rust}-{pluginName}-vX.Y.Z 格式的标签
- "wasm-*-*-v*.*.*" # 匹配 wasm-{go|rust}-{pluginName}-vX.Y.Z 格式的标签
workflow_dispatch:
inputs:
plugin_type:
description: 'Type of the plugin'
description: "Type of the plugin"
required: true
type: choice
options:
- go
- rust
plugin_name:
description: 'Name of the plugin'
description: "Name of the plugin"
required: true
type: string
version:
description: 'Version of the plugin (optional, without leading v)'
description: "Version of the plugin (optional, without leading v)"
required: false
type: string
@@ -31,8 +31,7 @@ jobs:
IMAGE_REGISTRY_SERVICE: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
IMAGE_REPOSITORY: ${{ vars.PLUGIN_IMAGE_REPOSITORY || 'plugins' }}
RUST_VERSION: 1.82
GO_VERSION: 1.19
TINYGO_VERSION: 0.28.1
GO_VERSION: 1.24.0
ORAS_VERSION: 1.0.0
steps:
- name: Set plugin_type, plugin_name and version from inputs or ref_name
@@ -53,7 +52,7 @@ jobs:
if [[ "$plugin_type" == "rust" ]]; then
builder_image="higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-rust-builder:rust${{ env.RUST_VERSION }}-oras${{ env.ORAS_VERSION }}"
else
builder_image="higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-go-builder:go${{ env.GO_VERSION }}-tinygo${{ env.TINYGO_VERSION }}-oras${{ env.ORAS_VERSION }}"
builder_image="higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-go-builder:go${{ env.GO_VERSION }}-oras${{ env.ORAS_VERSION }}"
fi
echo "PLUGIN_TYPE=$plugin_type" >> $GITHUB_ENV
echo "PLUGIN_NAME=$plugin_name" >> $GITHUB_ENV
@@ -62,9 +61,9 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
- name: File Check
run: |
run: |
workspace=${{ github.workspace }}/plugins/wasm-${PLUGIN_TYPE}/extensions/${PLUGIN_NAME}
push_command="./plugin.tar.gz:application/vnd.oci.image.layer.v1.tar+gzip"
@@ -79,7 +78,7 @@ jobs:
echo "README.md exists"
push_command="./README.md:application/vnd.module.wasm.doc.v1+markdown $push_command "
fi
# 查找README_{lang}.md
for file in ${workspace}/README_*.md; do
if [ -f "$file" ]; then
@@ -91,9 +90,9 @@ jobs:
done
echo "PUSH_COMMAND=\"$push_command\"" >> $GITHUB_ENV
- name: Run a wasm-builder
env:
env:
PLUGIN_NAME: ${{ env.PLUGIN_NAME }}
BUILDER_IMAGE: ${{ env.BUILDER_IMAGE }}
run: |
@@ -104,7 +103,7 @@ jobs:
push_command=${{ env.PUSH_COMMAND }}
push_command=${push_command#\"}
push_command=${push_command%\"} # 删除PUSH_COMMAND中的双引号确保oras push正常解析
target_image="${{ env.IMAGE_REGISTRY_SERVICE }}/${{ env.IMAGE_REPOSITORY}}/${{ env.PLUGIN_NAME }}:${{ env.VERSION }}"
target_image_latest="${{ env.IMAGE_REGISTRY_SERVICE }}/${{ env.IMAGE_REPOSITORY}}/${{ env.PLUGIN_NAME }}:latest"
echo "TargetImage=${target_image}"
@@ -123,7 +122,7 @@ jobs:
set -e
cd /workspace/plugins/wasm-go/extensions/${PLUGIN_NAME}
go mod tidy
tinygo build -o ./plugin.wasm -scheduler=none -target=wasi -gc=custom -tags=\"custommalloc nottinygc_finalizer ${EXTRA_TAGS}\" .
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o 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

@@ -2,20 +2,20 @@ name: "Build and Test Plugins"
on:
push:
branches: [ main ]
branches: [main]
paths:
- 'plugins/**'
- 'test/**'
- 'helm/**'
- 'Makefile.core.mk'
- "plugins/**"
- "test/**"
- "helm/**"
- "Makefile.core.mk"
pull_request:
branches: [ "*" ]
branches: ["*"]
paths:
- 'plugins/**'
- 'test/**'
- 'helm/**'
- 'Makefile.core.mk'
workflow_dispatch: ~
- "plugins/**"
- "test/**"
- "helm/**"
- "Makefile.core.mk"
workflow_dispatch: ~
jobs:
lint:
@@ -24,7 +24,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.22
go-version: 1.24
# There are too many lint errors in current code bases
# uncomment when we decide what lint should be addressed or ignored.
# - run: make lint
@@ -34,7 +34,7 @@ jobs:
strategy:
matrix:
# TODO(Xunzhuo): Enable C WASM Filters in CI
wasmPluginType: [ GO, RUST ]
wasmPluginType: [GO, RUST]
steps:
- uses: actions/checkout@v4
@@ -46,12 +46,12 @@ jobs:
dotnet: true
haskell: true
large-packages: true
swap-storage: true
swap-storage: true
- name: "Setup Go"
uses: actions/setup-go@v5
with:
go-version: 1.22
go-version: 1.24
- name: Setup Rust
uses: actions-rs/toolchain@v1
@@ -80,6 +80,6 @@ jobs:
publish:
runs-on: ubuntu-latest
needs: [ higress-wasmplugin-test ]
needs: [higress-wasmplugin-test]
steps:
- uses: actions/checkout@v4

View File

@@ -29,7 +29,7 @@ jobs:
echo "Version=$VERSION"
# Step 3
- name: Upload to OSS
uses: doggycool/ossutil-github-action@master
uses: go-choppy/ossutil-github-action@master
with:
ossArgs: 'cp -r -u ./artifact/ oss://higress-website-cn-hongkong/standalone/'
accessKey: ${{ secrets.ACCESS_KEYID }}

View File

@@ -17,7 +17,7 @@ jobs:
uses: actions/checkout@v4
# Step 2
- name: Download Helm Charts Index
uses: doggycool/ossutil-github-action@master
uses: go-choppy/ossutil-github-action@master
with:
ossArgs: 'cp oss://higress-website-cn-hongkong/helm-charts/index.yaml ./artifact/'
accessKey: ${{ secrets.ACCESS_KEYID }}
@@ -46,7 +46,7 @@ jobs:
sed -i 's/higress\.io/higress\.cn/g' ./artifact/cn-index.yaml
# Step 5
- name: Upload to OSS
uses: doggycool/ossutil-github-action@master
uses: go-choppy/ossutil-github-action@master
with:
ossArgs: 'cp -r -u ./artifact/ oss://higress-website-cn-hongkong/helm-charts/'
accessKey: ${{ secrets.ACCESS_KEYID }}

View File

@@ -0,0 +1,217 @@
name: Generate Release Notes
on:
push:
tags:
- "v*.*.*"
workflow_dispatch: ~
jobs:
generate-release-notes:
runs-on: ubuntu-latest
env:
DASHSCOPE_API_KEY: ${{ secrets.HIGRESS_OPENAI_API_KEY }}
MODEL_NAME: ${{ secrets.HIGRESS_OPENAI_API_MODEL }}
MODEL_SERVER: ${{ secrets.MODEL_SERVER }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.24
- name: Clone GitHub MCP Server
run: |
git clone https://github.com/github/github-mcp-server.git
cd github-mcp-server
go build -o ../github-mcp-serve ./cmd/github-mcp-server
cd ..
chmod u+x github-mcp-serve
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Clone Higress Report Agent
run: |
git clone https://github.com/higress-group/higress-report-agent.git
mv github-mcp-serve higress-report-agent/
- name: Clean up old release notes
run: |
RELEASE_VERSION=$(cat ${GITHUB_WORKSPACE}/VERSION)
CLEAN_VERSION=${RELEASE_VERSION#v}
if [ -d "release-notes/${CLEAN_VERSION}" ]; then
echo "Removing old release notes directory: release-notes/${CLEAN_VERSION}"
rm -rf release-notes/${CLEAN_VERSION}
else
echo "No old release notes directory found for version ${CLEAN_VERSION}."
fi
- name: Create Release Report Script
run: |
cat > generate_release_report.sh << 'EOF'
#!/bin/bash
# Script to generate release notes for Higress projects
echo "Fetching GitHub generated release notes for ${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}..."
curl -L \
"https://github.com/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/releases/tag/${RELEASE_VERSION}" \
-o release_page.html
echo "Extracting PR numbers from ${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME} release notes..."
PR_NUMS=$(cat release_page.html | grep -o "/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/pull/[0-9]*" | grep -o "[0-9]*$" | sort -n | uniq | tr '\n' ',')
PR_NUMS=${PR_NUMS%,}
if [ -z "${PR_NUMS}" ]; then
echo "No PR numbers found in release notes for ${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME} tag=${RELEASE_VERSION}."
rm release_page.html
exit 0
fi
echo "Identifying important PRs..."
IMPORTANT_PR_NUMS=$(cat release_page.html | grep -o "<strong>.*/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/pull/[0-9]*.*</strong>" | grep -o "pull/[0-9]*" | grep -o "[0-9]*" | sort -n | uniq | tr '\n' ',')
IMPORTANT_PR_NUMS=${IMPORTANT_PR_NUMS%,}
rm release_page.html
echo "Extracted PR numbers for ${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}: ${PR_NUMS}"
echo "Important PR numbers: ${IMPORTANT_PR_NUMS}"
echo "Generating detailed release notes for ${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}..."
cd higress-report-agent
pip install uv
uv sync
if [ -n "${IMPORTANT_PR_NUMS}" ]; then
uv run report_main.py --mode 2 --choice 2 --pr_nums ${PR_NUMS} --important_prs ${IMPORTANT_PR_NUMS}
else
uv run report_main.py --mode 2 --choice 2 --pr_nums ${PR_NUMS}
fi
cp report.md ../
cp report.EN.md ../
cd ..
# 去除主库版本号前缀v以主库版本号为路径
CLEAN_VERSION=${MAIN_RELEASE_VERSION#v}
echo "Creating release notes directory for main version ${MAIN_RELEASE_VERSION}..."
mkdir -p release-notes/${CLEAN_VERSION}
echo "# ${REPORT_TITLE}" >>release-notes/${CLEAN_VERSION}/README_ZH.md
sed 's/# Release Notes//' report.md >>release-notes/${CLEAN_VERSION}/README_ZH.md
echo -e "\n" >>release-notes/${CLEAN_VERSION}/README_ZH.md
echo "# ${REPORT_TITLE}" >>release-notes/${CLEAN_VERSION}/README.md
sed 's/# Release Notes//' report.EN.md >>release-notes/${CLEAN_VERSION}/README.md
echo -e "\n" >>release-notes/${CLEAN_VERSION}/README.md
rm report.md
rm report.EN.md
echo "${REPORT_TITLE} release notes saved to release-notes/${CLEAN_VERSION}/"
EOF
chmod +x generate_release_report.sh
- name: Generate Release Notes for Higress
env:
GITHUB_REPO_OWNER: alibaba
GITHUB_REPO_NAME: higress
GITHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPORT_TITLE: Higress
run: |
export MAIN_RELEASE_VERSION=$(cat ${GITHUB_WORKSPACE}/VERSION)
export RELEASE_VERSION=$(cat ${GITHUB_WORKSPACE}/VERSION)
bash generate_release_report.sh
- name: Generate Release Notes for Higress Console
env:
GITHUB_REPO_OWNER: higress-group
GITHUB_REPO_NAME: higress-console
GITHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPORT_TITLE: Higress Console
run: |
export MAIN_RELEASE_VERSION=$(cat ${GITHUB_WORKSPACE}/VERSION)
export RELEASE_VERSION=$(grep "^higress-console:" ${GITHUB_WORKSPACE}/DEP_VERSION | head -n1 | sed 's/higress-console: //')
bash generate_release_report.sh
- name: Create Update Release Notes Script
run: |
cat > update_release_note.sh << 'EOF'
#!/bin/bash
CLEAN_VERSION=${RELEASE_VERSION#v}
RELEASE_INFO=$(curl -s -L \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/releases/tags/${RELEASE_VERSION})
RELEASE_ID=$(echo $RELEASE_INFO | jq -r .id)
RELEASE_BODY=$(echo $RELEASE_INFO | jq -r .body)
NEW_CONTRIBUTORS=$(echo "$RELEASE_BODY" | awk '/## New Contributors/{flag=1; next} /\*\*Full Changelog\*\*/{flag=0} flag' | sed 's/\\n/\n/g')
FULL_CHANGELOG=$(echo "$RELEASE_BODY" | awk '/\*\*Full Changelog\*\*:/{print $0}' | sed 's/\*\*Full Changelog\*\*: //g' | sed 's/\\n/\n/g')
RELEASE_NOTES=$(cat release-notes/${CLEAN_VERSION}/README.md | sed 's/# /## /g')
if [[ -n "$NEW_CONTRIBUTORS" ]]; then
RELEASE_NOTES="${RELEASE_NOTES}
## New Contributors
${NEW_CONTRIBUTORS}"
fi
if [[ -n "$FULL_CHANGELOG" ]]; then
RELEASE_NOTES="${RELEASE_NOTES}
**Full Changelog**: ${FULL_CHANGELOG}"
fi
JSON_DATA=$(jq -n \
--arg body "$RELEASE_NOTES" \
'{body: $body}')
curl -L \
-X PATCH \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/releases/${RELEASE_ID} \
-d "$JSON_DATA"
EOF
chmod +x update_release_note.sh
- name: Update Release Notes
env:
GITHUB_REPO_OWNER: alibaba
GITHUB_REPO_NAME: higress
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
export RELEASE_VERSION=$(cat ${GITHUB_WORKSPACE}/VERSION)
bash update_release_note.sh
- name: Clean
run: |
rm generate_release_report.sh
rm update_release_note.sh
rm -rf higress-report-agent
rm -rf github-mcp-server
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Add release notes"
branch: add-release-notes
title: "Add release notes"
body: |
This PR adds release notes.
- Automatically generated by GitHub Actions
labels: release notes, automated
base: main

View File

@@ -6,11 +6,13 @@ on:
- "*"
paths:
- 'helm/**'
- '!helm/higress/README.zh.md'
workflow_dispatch: ~
push:
branches: [ main ]
paths:
- 'helm/**'
- '!helm/higress/README.zh.md'
jobs:
helm:
@@ -31,96 +33,9 @@ jobs:
run: |
GOBIN=$PWD GO111MODULE=on go install github.com/norwoodj/helm-docs/cmd/helm-docs@v1.14.2
./helm-docs -c ${GITHUB_WORKSPACE}/helm/higress -f ../core/values.yaml
DIFF=$(git diff ${GITHUB_WORKSPACE}/helm/higress/*md)
DIFF=$(git diff ${GITHUB_WORKSPACE}/helm/higress/README.md)
if [ ! -z "$DIFF" ]; then
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
translate-readme:
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: Compare README.md
id: compare_readme
run: |
cd ./helm/higress
BASE_BRANCH=main
UPSTREAM_REPO=https://github.com/alibaba/higress.git
TEMP_DIR=$(mktemp -d)
git clone --depth 1 --branch $BASE_BRANCH $UPSTREAM_REPO $TEMP_DIR
if diff -q "$TEMP_DIR/README.md" README.md > /dev/null; then
echo "README.md has no changes in comparison to base branch. Skipping translation."
echo "skip_translation=true" >> $GITHUB_ENV
else
echo "README.md has changed in comparison to base branch. Proceeding with translation."
echo "skip_translation=false" >> $GITHUB_ENV
fi
- name: Translate README.md to Chinese
if: env.skip_translation == 'false'
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"
echo "$RESPONSE" | jq -c -r '.choices[] | .message.content' > README.zh.new.md
if [ -f "README.zh.new.md" ]; then
echo "Translation completed and saved to README.zh.new.md."
else
echo "Translation failed or no content returned!"
exit 1
fi
mv README.zh.new.md README.zh.md
- name: Create Pull Request
if: env.skip_translation == 'false'
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Update helm translated README.zh.md"
branch: update-helm-readme-zh
title: "Update helm translated README.zh.md"
body: |
This PR updates the translated README.zh.md file.
- Automatically generated by GitHub Actions
labels: translation, automated
base: main

View File

@@ -17,7 +17,7 @@ jobs:
cat helm/core/crds/customresourcedefinitions.gen.yaml helm/core/crds/istio-envoyfilter.yaml > crd.yaml
- name: Upload hgctl packages to the GitHub release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
if: startsWith(github.ref, 'refs/tags/')
with:
files: |

View File

@@ -26,7 +26,7 @@ jobs:
zip -q -r hgctl_${{ env.HGCTL_VERSION }}_windows_arm64.zip out/windows_arm64/
- name: Upload hgctl packages to the GitHub release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
@@ -51,7 +51,7 @@ jobs:
tar -zcvf hgctl_${{ env.HGCTL_VERSION }}_darwin_arm64.tar.gz out/darwin_arm64/
- name: Upload hgctl packages to the GitHub release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
@@ -73,7 +73,7 @@ jobs:
tar -zcvf hgctl_${{ env.HGCTL_VERSION }}_darwin_amd64.tar.gz out/darwin_amd64/
- name: Upload hgctl packages to the GitHub release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
if: startsWith(github.ref, 'refs/tags/')
with:
files: |

36
.github/workflows/sync-crds.yaml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: "Sync CRDs to Helm Chart"
on:
workflow_dispatch: ~
push:
branches: [ main ]
paths:
- 'api/kubernetes/customresourcedefinitions.gen.yaml'
jobs:
sync-crds:
name: Sync CRDs
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Copy the CRD YAML File to Helm Folder
run: |
cp api/kubernetes/customresourcedefinitions.gen.yaml helm/core/crds/customresourcedefinitions.gen.yaml
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Update CRD file in the helm folder"
branch: sync-crds
title: "Update CRD file in the helm folder"
body: |
This PR updates CRD file in the helm folder.
- Automatically copied by GitHub Actions
labels: crds, automated
base: main

131
.github/workflows/translate-readme.yaml vendored Normal file
View File

@@ -0,0 +1,131 @@
name: "Helm Docs"
on:
workflow_dispatch: ~
push:
branches: [ main ]
paths:
- 'helm/higress/README.md'
jobs:
translate-readme:
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: Compare README.md
id: compare_readme
run: |
cd ./helm/higress
BASE_BRANCH=${GITHUB_BASE_REF:-main}
git fetch origin $BASE_BRANCH
if git diff --quiet origin/$BASE_BRANCH -- README.md; then
echo "README.md has no local changes compared to $BASE_BRANCH. Skipping translation."
echo "skip_translation=true" >> $GITHUB_ENV
else
echo "README.md has local changes compared to $BASE_BRANCH. Proceeding with translation."
echo "skip_translation=false" >> $GITHUB_ENV
echo "--------- diff ---------"
git diff origin/$BASE_BRANCH -- README.md
echo "------------------------"
fi
- name: Translate README.md to Chinese
if: env.skip_translation == 'false'
env:
API_URL: ${{ secrets.HIGRESS_OPENAI_API_URL }}
API_KEY: ${{ secrets.HIGRESS_OPENAI_API_KEY }}
API_MODEL: ${{ secrets.HIGRESS_OPENAI_API_MODEL }}
run: |
cat << 'EOF' > translate_readme.py
import os
import json
import requests
API_URL = os.environ["API_URL"]
API_KEY = os.environ["API_KEY"]
API_MODEL = os.environ["API_MODEL"]
README_PATH = "./helm/higress/README.md"
OUTPUT_PATH = "./helm/higress/README.zh.md"
def stream_translation(api_url, api_key, payload):
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
}
response = requests.post(api_url, headers=headers, json=payload, stream=True)
response.raise_for_status()
with open(OUTPUT_PATH, "w", encoding="utf-8") as out_file:
for line in response.iter_lines(decode_unicode=True):
if line.strip() == "" or not line.startswith("data: "):
continue
data = line[6:]
if data.strip() == "[DONE]":
break
try:
chunk = json.loads(data)
content = chunk["choices"][0]["delta"].get("content", "")
if content:
out_file.write(content)
except Exception as e:
print("Error parsing chunk:", e)
def main():
if not os.path.exists(README_PATH):
print("README.md not found!")
return
with open(README_PATH, "r", encoding="utf-8") as f:
content = f.read()
payload = {
"model": API_MODEL,
"messages": [
{
"role": "system",
"content": "You are a translation assistant that translates English Markdown text to Chinese. Preserve original Markdown formatting and line breaks."
},
{
"role": "user",
"content": content
}
],
"temperature": 0.3,
"stream": True
}
print("Streaming translation started...")
stream_translation(API_URL, API_KEY, payload)
print(f"Translation completed and saved to {OUTPUT_PATH}.")
if __name__ == "__main__":
main()
EOF
python3 translate_readme.py
rm -rf translate_readme.py
- name: Create Pull Request
if: env.skip_translation == 'false'
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Update helm translated README.zh.md"
branch: update-helm-readme-zh
title: "Update helm translated README.zh.md"
body: |
This PR updates the translated README.zh.md file.
- Automatically generated by GitHub Actions
labels: translation, automated
base: main

29
.github/workflows/translate-test.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: 'Translate GitHub content into English'
on:
issues:
types: [opened, edited]
issue_comment:
types: [created, edited]
discussion:
types: [created, edited]
discussion_comment:
types: [created, edited]
pull_request_target:
types: [opened, edited]
pull_request_review_comment:
types: [created, edited]
jobs:
translate:
permissions:
issues: write
discussions: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: lizheming/github-translate-action@main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
APPEND_TRANSLATION: true

View File

@@ -0,0 +1,378 @@
name: Wasm Plugin Unit Tests(GO)
on:
push:
branches: [ main ]
paths:
- 'plugins/wasm-go/extensions/**'
- '.github/workflows/wasm-plugin-unit-test.yml'
- 'go.mod'
- 'go.sum'
pull_request:
branches: [ "*" ]
paths:
- 'plugins/wasm-go/extensions/**'
- '.github/workflows/wasm-plugin-unit-test.yml'
- 'go.mod'
- 'go.sum'
env:
GO111MODULE: on
CGO_ENABLED: 0
GOOS: linux
GOARCH: amd64
jobs:
detect-changed-plugins:
name: Detect Changed Plugins
runs-on: ubuntu-latest
outputs:
changed-plugins: ${{ steps.detect.outputs.plugins }}
has-changes: ${{ steps.detect.outputs.has-changes }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # 获取完整历史用于比较
- name: Detect changed plugins
id: detect
run: |
# 获取变更的文件列表
if [ "${{ github.event_name }}" = "pull_request" ]; then
# PR模式比较目标分支和源分支
git fetch origin ${{ github.base_ref }}
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
else
# Push模式比较当前提交和上一个提交
CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD)
fi
echo "Changed files:"
echo "$CHANGED_FILES"
# 提取变更的插件名称
CHANGED_PLUGINS=""
for file in $CHANGED_FILES; do
if [[ $file =~ ^plugins/wasm-go/extensions/([^/]+)/ ]]; then
PLUGIN_NAME="${BASH_REMATCH[1]}"
if [[ ! " $CHANGED_PLUGINS " =~ " $PLUGIN_NAME " ]]; then
# 修复:只在非空时添加空格
if [ -z "$CHANGED_PLUGINS" ]; then
CHANGED_PLUGINS="$PLUGIN_NAME"
else
CHANGED_PLUGINS="$CHANGED_PLUGINS $PLUGIN_NAME"
fi
fi
fi
done
# 如果没有插件变更,不触发测试
if [ -z "$CHANGED_PLUGINS" ]; then
echo "No plugin changes detected, skipping tests"
echo "has-changes=false" >> $GITHUB_OUTPUT
echo "plugins=[]" >> $GITHUB_OUTPUT
else
echo "Changed plugins: $CHANGED_PLUGINS"
echo "has-changes=true" >> $GITHUB_OUTPUT
# 将空格分隔转换为 JSON 数组格式
PLUGINS_JSON=$(echo "$CHANGED_PLUGINS" | sed 's/ /","/g' | sed 's/^/["/' | sed 's/$/"]/')
echo "PLUGINS_JSON: $PLUGINS_JSON"
echo "plugins=$PLUGINS_JSON" >> $GITHUB_OUTPUT
fi
test:
name: Test Changed Plugins
runs-on: ubuntu-latest
needs: detect-changed-plugins
if: needs.detect-changed-plugins.outputs.has-changes == 'true'
strategy:
fail-fast: false
matrix:
plugin: ${{ fromJSON(needs.detect-changed-plugins.outputs.changed-plugins) }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go 1.24
uses: actions/setup-go@v4
with:
go-version: 1.24
cache: true
- name: Install test tools
run: |
go install gotest.tools/gotestsum@latest
# 移除gocov工具直接使用Codecov
- name: Build WASM for ${{ matrix.plugin }}
working-directory: plugins/wasm-go/extensions/${{ matrix.plugin }}
run: |
echo "Building WASM for ${{ matrix.plugin }}..."
# 检查是否存在main.go文件
export GOOS=wasip1
export GOARCH=wasm
# 构建WASM文件失败时直接退出
if ! go build -buildmode=c-shared -o main.wasm ./; then
echo "❌ WASM build failed for ${{ matrix.plugin }}"
exit 1
fi
# 验证WASM文件是否生成
if [ ! -f "main.wasm" ]; then
echo "❌ WASM file not generated for ${{ matrix.plugin }}"
exit 1
fi
echo "✅ WASM build successful for ${{ matrix.plugin }}"
- name: Set WASM_PATH environment variable
run: |
echo "WASM_PATH=$(pwd)/plugins/wasm-go/extensions/${{ matrix.plugin }}/main.wasm" >> $GITHUB_ENV
- name: Run tests with coverage for ${{ matrix.plugin }}
working-directory: plugins/wasm-go/extensions/${{ matrix.plugin }}
run: |
# 检查是否存在main_test.go文件
if [ -f "main_test.go" ]; then
echo "Running tests for ${{ matrix.plugin }}..."
# 运行测试并生成覆盖率报告
gotestsum --junitfile ../../../../test-results-${{ matrix.plugin }}.xml \
--format standard-verbose \
--jsonfile ../../../../test-output-${{ matrix.plugin }}.json \
-- -coverprofile=coverage-${{ matrix.plugin }}.out -covermode=atomic -coverpkg=./... ./...
echo "✅ Tests completed for ${{ matrix.plugin }}"
else
echo "No tests found for ${{ matrix.plugin }}, skipping..."
# 创建空的测试结果文件
echo '<?xml version="1.0" encoding="UTF-8"?><testsuites><testsuite name="no-tests" tests="0" failures="0" errors="0" time="0"></testsuite></testsuites>' > ../../../../test-results-${{ matrix.plugin }}.xml
fi
- name: Upload test results for ${{ matrix.plugin }}
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-${{ matrix.plugin }}
path: |
test-results-${{ matrix.plugin }}.xml
test-output-${{ matrix.plugin }}.json
retention-days: 30
- name: Upload coverage report for ${{ matrix.plugin }}
uses: actions/upload-artifact@v4
if: always()
with:
name: coverage-${{ matrix.plugin }}
path: plugins/wasm-go/extensions/${{ matrix.plugin }}/coverage-${{ matrix.plugin }}.out
retention-days: 30
test-summary:
name: Test Summary & Coverage
runs-on: ubuntu-latest
needs: [detect-changed-plugins, test]
if: always() && needs.detect-changed-plugins.outputs.has-changes == 'true'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go 1.24
uses: actions/setup-go@v4
with:
go-version: 1.24
cache: true
- name: Install required tools
run: |
go install github.com/wadey/gocovmerge@latest
- name: Download all test results
uses: actions/download-artifact@v4
with:
pattern: test-results-*
merge-multiple: true
path: ${{ github.workspace }}
- name: Download all coverage files
uses: actions/download-artifact@v4
with:
pattern: coverage-*
merge-multiple: true
path: ${{ github.workspace }}
- name: Generate comprehensive test summary
run: |
echo "## 🧪 Go Plugin Test Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
total_plugins=0
passed_plugins=0
failed_plugins=0
total_tests=0
total_failures=0
total_errors=0
echo "### 📊 Test Results by Plugin" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
for result_file in test-results-*.xml; do
if [ -f "$result_file" ]; then
plugin_name=$(echo "$result_file" | sed 's/test-results-\(.*\)\.xml/\1/')
total_plugins=$((total_plugins + 1))
# 解析XML获取测试结果
if grep -q '<testsuite' "$result_file"; then
# 使用grep解析XML属性更稳定可靠
tests=$(grep -o 'tests="[0-9]*"' "$result_file" | head -1 | grep -o '[0-9]*' || echo "0")
failures=$(grep -o 'failures="[0-9]*"' "$result_file" | head -1 | grep -o '[0-9]*' || echo "0")
errors=$(grep -o 'errors="[0-9]*"' "$result_file" | head -1 | grep -o '[0-9]*' || echo "0")
time=$(grep -o 'time="[0-9.]*"' "$result_file" | head -1 | grep -o '[0-9.]*' || echo "0")
# 确保数值有效避免bash算术运算错误
tests=${tests:-0}
failures=${failures:-0}
errors=${errors:-0}
# 转换为整数进行算术运算
total_tests=$((total_tests + tests))
total_failures=$((total_failures + failures))
total_errors=$((total_errors + errors))
if [ "$failures" = "0" ] && [ "$errors" = "0" ]; then
echo "✅ **$plugin_name**: $tests tests passed in ${time}s" >> $GITHUB_STEP_SUMMARY
passed_plugins=$((passed_plugins + 1))
else
echo "❌ **$plugin_name**: $tests tests, $failures failures, $errors errors in ${time}s" >> $GITHUB_STEP_SUMMARY
failed_plugins=$((failed_plugins + 1))
fi
else
echo "⚠️ **$plugin_name**: No tests found" >> $GITHUB_STEP_SUMMARY
fi
fi
done
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 📈 Coverage Report" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "📊 **Coverage reports are now available on Codecov**" >> $GITHUB_STEP_SUMMARY
echo "🔗 **This Commit Coverage**: https://codecov.io/gh/${{ github.repository }}/commit/${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 🎯 Summary" >> $GITHUB_STEP_SUMMARY
echo "- **Total plugins**: $total_plugins" >> $GITHUB_STEP_SUMMARY
echo "- **Passed**: $passed_plugins ✅" >> $GITHUB_STEP_SUMMARY
echo "- **Failed**: $failed_plugins ❌" >> $GITHUB_STEP_SUMMARY
echo "- **Total tests**: $total_tests" >> $GITHUB_STEP_SUMMARY
echo "- **Total failures**: $total_failures" >> $GITHUB_STEP_SUMMARY
echo "- **Total errors**: $total_errors" >> $GITHUB_STEP_SUMMARY
# 如果有失败,显示详细信息
if [ $total_failures -gt 0 ] || [ $total_errors -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "### ❌ Failed Tests Details" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Failed plugins**: $failed_plugins" >> $GITHUB_STEP_SUMMARY
echo "**Total failures**: $total_failures" >> $GITHUB_STEP_SUMMARY
echo "**Total errors**: $total_errors" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "📋 **View detailed logs**: [Click here](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# 显示每个失败插件的详细信息
echo "#### 📊 Failed Plugin Details" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
for result_file in test-results-*.xml; do
if [ -f "$result_file" ]; then
plugin_name=$(echo "$result_file" | sed 's/test-results-\(.*\)\.xml/\1/')
# 检查是否有失败
failures=$(grep -o 'failures="[0-9]*"' "$result_file" | head -1 | grep -o '[0-9]*' || echo "0")
errors=$(grep -o 'errors="[0-9]*"' "$result_file" | head -1 | grep -o '[0-9]*' || echo "0")
# 确保数值有效
failures=${failures:-0}
errors=${errors:-0}
if [ "$failures" -gt 0 ] || [ "$errors" -gt 0 ]; then
echo "**$plugin_name**:" >> $GITHUB_STEP_SUMMARY
echo "- Failures: $failures" >> $GITHUB_STEP_SUMMARY
echo "- Errors: $errors" >> $GITHUB_STEP_SUMMARY
echo "- [View plugin logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
fi
done
fi
# - name: Merge coverage reports
# run: |
# echo "Merging coverage reports..."
#
# # 使用绝对路径查找,更可靠
# coverage_files=$(find ${{ github.workspace }} -name "coverage-*")
#
# if [ -n "$coverage_files" ]; then
# echo "Found coverage files:"
# echo "$coverage_files"
#
# # 使用gocovmerge顺序合并
# echo "Merging Go coverage files using gocovmerge sequential method..."
#
# # 将文件列表转换为数组
# readarray -t coverage_array <<< "$coverage_files"
# file_count=${#coverage_array[@]}
#
# echo "Total files to merge: $file_count"
#
# # 复制第一个文件作为基础
# cp "${coverage_array[0]}" ${{ github.workspace }}/merged_coverage.out
# echo "Starting with: ${coverage_array[0]}"
#
# # 如果有多个文件,逐个合并其他文件到最终目标
# if [ $file_count -gt 1 ]; then
# echo "Multiple files, merging sequentially with gocovmerge..."
#
# for ((i=1; i<file_count; i++)); do
# current_file="${coverage_array[i]}"
#
# echo "Merging file $((i+1))/$file_count: $current_file"
#
# # 使用gocovmerge合并到最终目标文件
# gocovmerge "${{ github.workspace }}/merged_coverage.out" "$current_file" > "${{ github.workspace }}/temp_merge.out"
# mv "${{ github.workspace }}/temp_merge.out" "${{ github.workspace }}/merged_coverage.out"
#
# echo "Successfully merged with $current_file"
# done
# fi
#
# echo "Coverage reports merged successfully using gocovmerge sequential method"
# echo "Merged file size: $(wc -c < ${{ github.workspace }}/merged_coverage.out) bytes"
# else
# echo "No coverage files found"
# # 创建空的覆盖率文件
# echo "mode: atomic" > ${{ github.workspace }}/merged_coverage.out
# fi
# - name: Upload merged coverage to Codecov
# uses: codecov/codecov-action@v4
# if: always()
# with:
# file: ${{ github.workspace }}/merged_coverage.out
# flags: wasm-go-plugins-tests
# name: codecov-wasm-go-plugins
# fail_ci_if_error: false
# verbose: true

View File

@@ -27,12 +27,14 @@ header:
- 'plugins/**'
- 'CODEOWNERS'
- 'VERSION'
- 'DEP_VERSION'
- 'tools/'
- 'test/README.md'
- 'test/README_CN.md'
- 'hgctl/cmd/hgctl/config/testdata/config'
- 'hgctl/pkg/manifests'
- 'pkg/ingress/kube/gateway/istio/testdata'
- 'release-notes/**'
comment: on-failure
dependency:

View File

@@ -2,10 +2,10 @@
/envoy @gengleilei @johnlanni
/istio @SpecialYang @johnlanni
/pkg @SpecialYang @johnlanni @CH3CHO
/plugins @johnlanni @CH3CHO @rinfx
/plugins/wasm-go/extensions/ai-proxy @cr7258 @CH3CHO @rinfx
/plugins @johnlanni @CH3CHO @rinfx @erasernoob
/plugins/wasm-go/extensions/ai-proxy @rinfx @wydream @johnlanni
/plugins/wasm-rust @007gzs @jizhuozhi
/registry @NameHaibinZhang @2456868764 @johnlanni
/registry @Erica177 @2456868764 @johnlanni
/test @Xunzhuo @2456868764 @CH3CHO
/tools @johnlanni @Xunzhuo @2456868764

1
DEP_VERSION Normal file
View File

@@ -0,0 +1 @@
higress-console: v2.1.7

View File

@@ -137,6 +137,8 @@ endif
# for now docker is limited to Linux compiles - why ?
include docker/docker.mk
docker-build-amd64: docker.higress-amd64 ## Build and push amdd64 docker images to registry defined by $HUB and $TAG
docker-build: docker.higress ## Build and push docker images to registry defined by $HUB and $TAG
docker-buildx-push: clean-env docker.higress-buildx
@@ -144,7 +146,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.4/envoy-symbol-ARCH.tar.gz
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.1.9/envoy-symbol-ARCH.tar.gz
build-envoy: prebuild
./tools/hack/build-envoy.sh
@@ -191,8 +193,9 @@ install: pre-install
cd helm/higress; helm dependency build
helm install higress helm/higress -n higress-system --create-namespace --set 'global.local=true'
ENVOY_LATEST_IMAGE_TAG ?= 958467a353d411ae3f06e03b096bfd342cddb2c6
ISTIO_LATEST_IMAGE_TAG ?= d9c728d3b01f64855e012b08d136e306f1160397
HIGRESS_LATEST_IMAGE_TAG ?= latest
ENVOY_LATEST_IMAGE_TAG ?= 48da465cfd0dc5c9ac851bd2b9743780dc82dd8a
ISTIO_LATEST_IMAGE_TAG ?= latest
install-dev: pre-install
helm install higress helm/core -n higress-system --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'pilot.tag=$(ISTIO_LATEST_IMAGE_TAG)' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true'
@@ -268,10 +271,26 @@ higress-conformance-test-clean: $(tools/kind) delete-cluster
.PHONY: higress-wasmplugin-test-prepare
higress-wasmplugin-test-prepare: $(tools/kind) delete-cluster create-cluster docker-build kube-load-image install-dev-wasmplugin
# higress-wasmplugin-test-prepare-skip-docker-build prepares the environment for higress wasmplugin tests without build higress docker image.
.PHONY: higress-wasmplugin-test-prepare-skip-docker-build
higress-wasmplugin-test-prepare-skip-docker-build: $(tools/kind) delete-cluster create-cluster prebuild
@export TAG="$(HIGRESS_LATEST_IMAGE_TAG)" && \
$(MAKE) kube-load-image && \
$(MAKE) install-dev-wasmplugin
# higress-wasmplugin-test runs ingress wasmplugin tests.
.PHONY: higress-wasmplugin-test
higress-wasmplugin-test: $(tools/kind) delete-cluster create-cluster docker-build kube-load-image install-dev-wasmplugin run-higress-e2e-test-wasmplugin delete-cluster
# higress-wasmplugin-test-skip-docker-build runs ingress wasmplugin tests without build higress docker image
.PHONY: higress-wasmplugin-test-skip-docker-build
higress-wasmplugin-test-skip-docker-build: $(tools/kind) delete-cluster create-cluster prebuild
@export TAG="$(HIGRESS_LATEST_IMAGE_TAG)" && \
$(MAKE) kube-load-image && \
$(MAKE) install-dev-wasmplugin && \
$(MAKE) run-higress-e2e-test-wasmplugin && \
$(MAKE) delete-cluster
# higress-wasmplugin-test-clean cleans the environment for higress wasmplugin tests.
.PHONY: higress-wasmplugin-test-clean
higress-wasmplugin-test-clean: $(tools/kind) delete-cluster
@@ -290,8 +309,12 @@ delete-cluster: $(tools/kind) ## Delete kind cluster.
# dubbo-provider-demo和nacos-standlone-rc3的镜像已经上传到阿里云镜像库第一次需要先拉到本地
# docker pull registry.cn-hangzhou.aliyuncs.com/hinsteny/dubbo-provider-demo:0.0.1
# docker pull registry.cn-hangzhou.aliyuncs.com/hinsteny/nacos-standlone-rc3:1.0.0-RC3
# If TAG is HIGRESS_LATEST_IMAGE_TAG, means we skip building higress docker image, so we need to pull the image first.
.PHONY: kube-load-image
kube-load-image: $(tools/kind) ## Install the Higress image to a kind cluster using the provided $IMAGE and $TAG.
@if [ "$(TAG)" = "$(HIGRESS_LATEST_IMAGE_TAG)" ]; then \
tools/hack/docker-pull-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/higress $(TAG); \
fi
tools/hack/kind-load-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/higress $(TAG)
tools/hack/docker-pull-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/pilot $(ISTIO_LATEST_IMAGE_TAG)
tools/hack/docker-pull-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway $(ENVOY_LATEST_IMAGE_TAG)

View File

@@ -10,8 +10,10 @@
[![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)
[![discord](https://img.shields.io/discord/1364956090566971515?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square)](https://discord.gg/tSbww9VDaM)
<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> <a href="https://www.producthunt.com/posts/higress?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-higress" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=951287&theme=light&t=1745492822283" alt="Higress - Global&#0032;APIs&#0032;as&#0032;MCP&#0032;powered&#0032;by&#0032;AI&#0032;Gateway | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<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>
[**Official Site**](https://higress.ai/en/) &nbsp; |
@@ -22,13 +24,21 @@
English | <a href="README_ZH.md">中文<a/> | <a href="README_JP.md">日本語<a/>
</p>
## What is Higress?
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 was born within Alibaba to solve the issues of Tengine reload affecting long-connection services and insufficient load balancing capabilities for gRPC/Dubbo.
### Core Use Cases
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. It also supports hosting MCP (Model Context Protocol) Servers through its plugin mechanism, enabling AI Agents to easily call various tools and services. With the [openapi-to-mcp tool](https://github.com/higress-group/openapi-to-mcpserver), you can quickly convert OpenAPI specifications into remote MCP servers for hosting. Higress provides unified management for both LLM API and MCP API.
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).
**🌟 Try it now at [https://mcp.higress.ai/](https://mcp.higress.ai/)** to experience Higress-hosted Remote MCP Servers firsthand:
![Higress MCP Server Platform](https://img.alicdn.com/imgextra/i2/O1CN01nmVa0a1aChgpyyWOX_!!6000000003294-0-tps-3430-1742.jpg)
### Enterprise Adoption
Higress was born within Alibaba to solve the issues of Tengine reload affecting long-connection services and insufficient load balancing capabilities for gRPC/Dubbo. Within Alibaba Cloud, Higress's AI gateway capabilities support core AI applications such as Tongyi Bailian model studio, machine learning PAI platform, and other critical AI services. 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.
## Summary
@@ -59,37 +69,37 @@ Port descriptions:
> All Higress Docker images use Higress's own image repository and are not affected by Docker Hub rate limits.
> In addition, the submission and updates of the images are protected by a security scanning mechanism (powered by Alibaba Cloud ACR), making them very secure for use in production environments.
>
> If you experience a timeout when pulling image from `higress-registry.cn-hangzhou.cr.aliyuncs.com`, you can try replacing it with the following docker registry mirror source:
>
> **Southeast Asia**: `higress-registry.ap-southeast-7.cr.aliyuncs.com`
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).
## Use Cases
- **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)
- **MCP Server Hosting**:
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.
Higress hosts MCP Servers through its plugin mechanism, enabling AI Agents to easily call various tools and services. With the [openapi-to-mcp tool](https://github.com/higress-group/openapi-to-mcpserver), you can quickly convert OpenAPI specifications into remote MCP servers.
![](https://img.alicdn.com/imgextra/i1/O1CN01wv8H4g1mS4MUzC1QC_!!6000000004952-2-tps-1764-597.png)
**🌟 Try it now!** Experience Higress-hosted Remote MCP Servers at [https://mcp.higress.ai/](https://mcp.higress.ai/)
![Higress MCP Server Platform](https://img.alicdn.com/imgextra/i2/O1CN01nmVa0a1aChgpyyWOX_!!6000000003294-0-tps-3430-1742.jpg)
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
Key benefits of hosting MCP Servers with Higress:
- Unified authentication and authorization mechanisms
- Fine-grained rate limiting to prevent abuse
- Comprehensive audit logs for all tool calls
- Rich observability for monitoring performance
- Simplified deployment through Higress's plugin mechanism
- Dynamic updates without disruption or connection drops
[Learn more...](https://higress.cn/en/ai/mcp-quick-start/?spm=36971b57.7beea2de.0.0.d85f20a94jsWGm)
- **AI Gateway**:
Higress connects to all LLM model providers using a unified protocol, with AI observability, multi-model load balancing, token rate limiting, and caching capabilities:
![](https://img.alicdn.com/imgextra/i2/O1CN01izmBNX1jbHT7lP3Yr_!!6000000004566-0-tps-1920-1080.jpg)
- **Kubernetes ingress controller**:
Higress can function as a feature-rich ingress controller, which is compatible with many annotations of K8s' nginx ingress controller.
@@ -135,7 +145,10 @@ For other installation methods such as Helm deployment under K8s, please refer t
## Community
[Slack](https://w1689142780-euk177225.slack.com/archives/C05GEL4TGTG): to get invited go [here](https://communityinviter.com/apps/w1689142780-euk177225/higress).
Join our Discord community! This is where you can connect with developers and other enthusiastic users of Higress.
[![discord](https://img.shields.io/discord/1364956090566971515?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge)](https://discord.gg/tSbww9VDaM)
### Thanks

View File

@@ -22,15 +22,21 @@
</p>
## Higressとは
Higressは、IstioとEnvoyをベースにしたクラウドネイティブAPIゲートウェイで、Go/Rust/JSなどを使用してWasmプラグインを作成できます。数十の既製の汎用プラグインと、すぐに使用できるコンソールを提供していますデモは[こちら](http://demo.higress.io/))。
Higressは、Tengineのリロードが長時間接続のビジネスに影響を与える問題や、gRPC/Dubboの負荷分散能力の不足を解決するために、Alibaba内部で誕生しました。
### 主な使用シナリオ
Alibaba Cloudは、Higressを基盤にクラウドネイティブAPIゲートウェイ製品を構築し、多くの企業顧客に99.99%のゲートウェイ高可用性保証サービスを提供しています。
HigressのAIゲートウェイ機能は、国内外のすべての[主要モデルプロバイダー](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider)をサポートし、vllm/ollamaなどに基づく自己構築DeepSeekモデルにも対応しています。また、プラグインメカニズムを通じてMCPModel Context Protocolサーバーをホストすることもでき、AI Agentが様々なツールやサービスを簡単に呼び出せるようにします。[openapi-to-mcpツール](https://github.com/higress-group/openapi-to-mcpserver)を使用すると、OpenAPI仕様を迅速にリモートMCPサーバーに変換してホスティングできます。HigressはLLM APIとMCP APIの統一管理を提供します。
Higressは、AIゲートウェイ機能を基盤に、Tongyi Qianwen APP、Bailian大規模モデルAPI、機械学習PAIプラットフォームなどのAIビジネスをサポートしています。また、国内の主要なAIGC企業ZeroOneやAI製品FastGPTにもサービスを提供しています
**🌟 今すぐ[https://mcp.higress.ai/](https://mcp.higress.ai/)で体験**してください。HigressがホストするリモートMCPサーバーを直接体験できます:
![](https://img.alicdn.com/imgextra/i2/O1CN011AbR8023V8R5N0HcA_!!6000000007260-2-tps-1080-606.png)
![Higress MCP Server Platform](https://img.alicdn.com/imgextra/i2/O1CN01nmVa0a1aChgpyyWOX_!!6000000003294-0-tps-3430-1742.jpg)
### 企業での採用
Higressは、Tengineのリロードが長時間接続のビジネスに影響を与える問題や、gRPC/Dubboの負荷分散能力の不足を解決するために、Alibaba内部で誕生しました。Alibaba Cloud内では、HigressのAIゲートウェイ機能がTongyi Qianwen APP、Tongyi Bailian Model Studio、機械学習PAIプラットフォームなどの中核的なAIアプリケーションをサポートしています。また、国内の主要なAIGC企業ZeroOneやAI製品FastGPTにもサービスを提供しています。Alibaba Cloudは、Higressを基盤にクラウドネイティブAPIゲートウェイ製品を構築し、多くの企業顧客に99.99%のゲートウェイ高可用性保証サービスを提供しています。
## 目次
@@ -79,10 +85,6 @@ K8sでのHelmデプロイなどの他のインストール方法については
![](https://img.alicdn.com/imgextra/i3/O1CN01K4qPUX1OliZa8KIPw_!!6000000001746-2-tps-1581-615.png)
**🌟 今すぐ試してみよう!** [https://mcp.higress.ai/](https://mcp.higress.ai/) でHigressがホストするリモートMCPサーバーを体験できます。このプラットフォームでは、HigressがどのようにリモートMCPサーバーをホストおよび管理するかを直接体験できます。
![Higress MCP サーバープラットフォーム](https://img.alicdn.com/imgextra/i2/O1CN01nmVa0a1aChgpyyWOX_!!6000000003294-0-tps-3430-1742.jpg)
Higressを使用してMCP Serverをホストすることで、以下のことが実現できます
- 統一された認証と認可メカニズム、AIツール呼び出しのセキュリティを確保
- きめ細かいレート制限、乱用やリソース枯渇を防止

View File

@@ -11,7 +11,7 @@
[![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>
<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> <a href="https://www.producthunt.com/posts/higress?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-higress" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=951287&theme=light&t=1745492822283" alt="Higress - Global&#0032;APIs&#0032;as&#0032;MCP&#0032;powered&#0032;by&#0032;AI&#0032;Gateway | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</div>
[**官网**](https://higress.cn/) &nbsp; |
@@ -28,15 +28,21 @@
</p>
## Higress 是什么?
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 模型。同时Higress 支持通过插件方式托管 MCP (Model Context Protocol) 服务器,使 AI Agent 能够更容易地调用各种工具和服务。借助 [openapi-to-mcp 工具](https://github.com/higress-group/openapi-to-mcpserver),您可以快速将 OpenAPI 规范转换为远程 MCP 服务器进行托管。Higress 提供了对 LLM API 和 MCP API 的统一管理
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://mcp.higress.ai/](https://mcp.higress.ai/)** 基于 Higress 托管的远程 MCP 服务器:
![](https://img.alicdn.com/imgextra/i2/O1CN011AbR8023V8R5N0HcA_!!6000000007260-2-tps-1080-606.png)
![Higress MCP 服务器平台](https://img.alicdn.com/imgextra/i2/O1CN01nmVa0a1aChgpyyWOX_!!6000000003294-0-tps-3430-1742.jpg)
### 生产环境采用
Higress 在阿里内部为解决 Tengine reload 对长连接业务有损,以及 gRPC/Dubbo 负载均衡能力不足而诞生。在阿里云内部Higress 的 AI 网关能力支撑了通义千问 APP、通义百炼模型工作室、机器学习 PAI 平台等核心 AI 应用。同时服务国内头部的 AIGC 企业(如零一万物),以及 AI 产品(如 FastGPT。阿里云基于 Higress 构建了云原生 API 网关产品,为大量企业客户提供 99.99% 的网关高可用保障服务能力。
## Summary
@@ -89,10 +95,6 @@ K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start
![](https://img.alicdn.com/imgextra/i3/O1CN01K4qPUX1OliZa8KIPw_!!6000000001746-2-tps-1581-615.png)
**🌟 立即体验!** 在 [https://mcp.higress.ai/](https://mcp.higress.ai/) 体验 Higress 托管的远程 MCP 服务器。这个平台让您可以体验基于 Higress 托管的远程 MCP 服务器的效果。
![Higress MCP 服务器平台](https://img.alicdn.com/imgextra/i2/O1CN01nmVa0a1aChgpyyWOX_!!6000000003294-0-tps-3430-1742.jpg)
通过 Higress 托管 MCP Server可以实现
- 统一的认证和鉴权机制,确保 AI 工具调用的安全性
- 精细化的速率限制,防止滥用和资源耗尽

View File

@@ -1 +1 @@
v2.1.1
v2.1.7

View File

@@ -247,9 +247,30 @@ spec:
properties:
spec:
properties:
proxies:
items:
properties:
connectTimeout:
type: integer
listenerPort:
type: integer
name:
type: string
serverAddress:
type: string
serverPort:
type: integer
type:
type: string
type: object
type: array
registries:
items:
properties:
allowMcpServers:
items:
type: string
type: array
authSecretName:
type: string
consulDatacenter:
@@ -263,6 +284,25 @@ spec:
type: string
domain:
type: string
enableMCPServer:
type: boolean
enableScopeMcpServers:
type: boolean
mcpServerBaseUrl:
type: string
mcpServerExportDomains:
items:
type: string
type: array
metadata:
additionalProperties:
properties:
innerMap:
additionalProperties:
type: string
type: object
type: object
type: object
nacosAccessKey:
type: string
nacosAddressServer:
@@ -286,6 +326,8 @@ spec:
type: integer
protocol:
type: string
proxyName:
type: string
sni:
type: string
type:

View File

@@ -26,6 +26,8 @@
package v1
import (
_ "github.com/golang/protobuf/ptypes/struct"
wrappers "github.com/golang/protobuf/ptypes/wrappers"
_ "google.golang.org/genproto/googleapis/api/annotations"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
@@ -63,6 +65,7 @@ type McpBridge struct {
unknownFields protoimpl.UnknownFields
Registries []*RegistryConfig `protobuf:"bytes,1,rep,name=registries,proto3" json:"registries,omitempty"`
Proxies []*ProxyConfig `protobuf:"bytes,2,rep,name=proxies,proto3" json:"proxies,omitempty"`
}
func (x *McpBridge) Reset() {
@@ -104,30 +107,44 @@ func (x *McpBridge) GetRegistries() []*RegistryConfig {
return nil
}
func (x *McpBridge) GetProxies() []*ProxyConfig {
if x != nil {
return x.Proxies
}
return nil
}
type RegistryConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Domain string `protobuf:"bytes,3,opt,name=domain,proto3" json:"domain,omitempty"`
Port uint32 `protobuf:"varint,4,opt,name=port,proto3" json:"port,omitempty"`
NacosAddressServer string `protobuf:"bytes,5,opt,name=nacosAddressServer,proto3" json:"nacosAddressServer,omitempty"`
NacosAccessKey string `protobuf:"bytes,6,opt,name=nacosAccessKey,proto3" json:"nacosAccessKey,omitempty"`
NacosSecretKey string `protobuf:"bytes,7,opt,name=nacosSecretKey,proto3" json:"nacosSecretKey,omitempty"`
NacosNamespaceId string `protobuf:"bytes,8,opt,name=nacosNamespaceId,proto3" json:"nacosNamespaceId,omitempty"`
NacosNamespace string `protobuf:"bytes,9,opt,name=nacosNamespace,proto3" json:"nacosNamespace,omitempty"`
NacosGroups []string `protobuf:"bytes,10,rep,name=nacosGroups,proto3" json:"nacosGroups,omitempty"`
NacosRefreshInterval int64 `protobuf:"varint,11,opt,name=nacosRefreshInterval,proto3" json:"nacosRefreshInterval,omitempty"`
ConsulNamespace string `protobuf:"bytes,12,opt,name=consulNamespace,proto3" json:"consulNamespace,omitempty"`
ZkServicesPath []string `protobuf:"bytes,13,rep,name=zkServicesPath,proto3" json:"zkServicesPath,omitempty"`
ConsulDatacenter string `protobuf:"bytes,14,opt,name=consulDatacenter,proto3" json:"consulDatacenter,omitempty"`
ConsulServiceTag string `protobuf:"bytes,15,opt,name=consulServiceTag,proto3" json:"consulServiceTag,omitempty"`
ConsulRefreshInterval int64 `protobuf:"varint,16,opt,name=consulRefreshInterval,proto3" json:"consulRefreshInterval,omitempty"`
AuthSecretName string `protobuf:"bytes,17,opt,name=authSecretName,proto3" json:"authSecretName,omitempty"`
Protocol string `protobuf:"bytes,18,opt,name=protocol,proto3" json:"protocol,omitempty"`
Sni string `protobuf:"bytes,19,opt,name=sni,proto3" json:"sni,omitempty"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Domain string `protobuf:"bytes,3,opt,name=domain,proto3" json:"domain,omitempty"`
Port uint32 `protobuf:"varint,4,opt,name=port,proto3" json:"port,omitempty"`
NacosAddressServer string `protobuf:"bytes,5,opt,name=nacosAddressServer,proto3" json:"nacosAddressServer,omitempty"`
NacosAccessKey string `protobuf:"bytes,6,opt,name=nacosAccessKey,proto3" json:"nacosAccessKey,omitempty"`
NacosSecretKey string `protobuf:"bytes,7,opt,name=nacosSecretKey,proto3" json:"nacosSecretKey,omitempty"`
NacosNamespaceId string `protobuf:"bytes,8,opt,name=nacosNamespaceId,proto3" json:"nacosNamespaceId,omitempty"`
NacosNamespace string `protobuf:"bytes,9,opt,name=nacosNamespace,proto3" json:"nacosNamespace,omitempty"`
NacosGroups []string `protobuf:"bytes,10,rep,name=nacosGroups,proto3" json:"nacosGroups,omitempty"`
NacosRefreshInterval int64 `protobuf:"varint,11,opt,name=nacosRefreshInterval,proto3" json:"nacosRefreshInterval,omitempty"`
ConsulNamespace string `protobuf:"bytes,12,opt,name=consulNamespace,proto3" json:"consulNamespace,omitempty"`
ZkServicesPath []string `protobuf:"bytes,13,rep,name=zkServicesPath,proto3" json:"zkServicesPath,omitempty"`
ConsulDatacenter string `protobuf:"bytes,14,opt,name=consulDatacenter,proto3" json:"consulDatacenter,omitempty"`
ConsulServiceTag string `protobuf:"bytes,15,opt,name=consulServiceTag,proto3" json:"consulServiceTag,omitempty"`
ConsulRefreshInterval int64 `protobuf:"varint,16,opt,name=consulRefreshInterval,proto3" json:"consulRefreshInterval,omitempty"`
AuthSecretName string `protobuf:"bytes,17,opt,name=authSecretName,proto3" json:"authSecretName,omitempty"`
Protocol string `protobuf:"bytes,18,opt,name=protocol,proto3" json:"protocol,omitempty"`
Sni string `protobuf:"bytes,19,opt,name=sni,proto3" json:"sni,omitempty"`
McpServerExportDomains []string `protobuf:"bytes,20,rep,name=mcpServerExportDomains,proto3" json:"mcpServerExportDomains,omitempty"`
McpServerBaseUrl string `protobuf:"bytes,21,opt,name=mcpServerBaseUrl,proto3" json:"mcpServerBaseUrl,omitempty"`
EnableMCPServer *wrappers.BoolValue `protobuf:"bytes,22,opt,name=enableMCPServer,proto3" json:"enableMCPServer,omitempty"`
EnableScopeMcpServers *wrappers.BoolValue `protobuf:"bytes,23,opt,name=enableScopeMcpServers,proto3" json:"enableScopeMcpServers,omitempty"`
AllowMcpServers []string `protobuf:"bytes,24,rep,name=allowMcpServers,proto3" json:"allowMcpServers,omitempty"`
Metadata map[string]*InnerMap `protobuf:"bytes,25,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
ProxyName string `protobuf:"bytes,26,opt,name=proxyName,proto3" json:"proxyName,omitempty"`
}
func (x *RegistryConfig) Reset() {
@@ -295,6 +312,189 @@ func (x *RegistryConfig) GetSni() string {
return ""
}
func (x *RegistryConfig) GetMcpServerExportDomains() []string {
if x != nil {
return x.McpServerExportDomains
}
return nil
}
func (x *RegistryConfig) GetMcpServerBaseUrl() string {
if x != nil {
return x.McpServerBaseUrl
}
return ""
}
func (x *RegistryConfig) GetEnableMCPServer() *wrappers.BoolValue {
if x != nil {
return x.EnableMCPServer
}
return nil
}
func (x *RegistryConfig) GetEnableScopeMcpServers() *wrappers.BoolValue {
if x != nil {
return x.EnableScopeMcpServers
}
return nil
}
func (x *RegistryConfig) GetAllowMcpServers() []string {
if x != nil {
return x.AllowMcpServers
}
return nil
}
func (x *RegistryConfig) GetMetadata() map[string]*InnerMap {
if x != nil {
return x.Metadata
}
return nil
}
func (x *RegistryConfig) GetProxyName() string {
if x != nil {
return x.ProxyName
}
return ""
}
type ProxyConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
ServerAddress string `protobuf:"bytes,3,opt,name=serverAddress,proto3" json:"serverAddress,omitempty"`
ServerPort uint32 `protobuf:"varint,4,opt,name=serverPort,proto3" json:"serverPort,omitempty"`
ListenerPort uint32 `protobuf:"varint,5,opt,name=listenerPort,proto3" json:"listenerPort,omitempty"`
ConnectTimeout uint32 `protobuf:"varint,6,opt,name=connectTimeout,proto3" json:"connectTimeout,omitempty"`
}
func (x *ProxyConfig) Reset() {
*x = ProxyConfig{}
if protoimpl.UnsafeEnabled {
mi := &file_networking_v1_mcp_bridge_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ProxyConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ProxyConfig) ProtoMessage() {}
func (x *ProxyConfig) ProtoReflect() protoreflect.Message {
mi := &file_networking_v1_mcp_bridge_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ProxyConfig.ProtoReflect.Descriptor instead.
func (*ProxyConfig) Descriptor() ([]byte, []int) {
return file_networking_v1_mcp_bridge_proto_rawDescGZIP(), []int{2}
}
func (x *ProxyConfig) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *ProxyConfig) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *ProxyConfig) GetServerAddress() string {
if x != nil {
return x.ServerAddress
}
return ""
}
func (x *ProxyConfig) GetServerPort() uint32 {
if x != nil {
return x.ServerPort
}
return 0
}
func (x *ProxyConfig) GetListenerPort() uint32 {
if x != nil {
return x.ListenerPort
}
return 0
}
func (x *ProxyConfig) GetConnectTimeout() uint32 {
if x != nil {
return x.ConnectTimeout
}
return 0
}
type InnerMap struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
InnerMap map[string]string `protobuf:"bytes,1,rep,name=inner_map,json=innerMap,proto3" json:"inner_map,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}
func (x *InnerMap) Reset() {
*x = InnerMap{}
if protoimpl.UnsafeEnabled {
mi := &file_networking_v1_mcp_bridge_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *InnerMap) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*InnerMap) ProtoMessage() {}
func (x *InnerMap) ProtoReflect() protoreflect.Message {
mi := &file_networking_v1_mcp_bridge_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use InnerMap.ProtoReflect.Descriptor instead.
func (*InnerMap) Descriptor() ([]byte, []int) {
return file_networking_v1_mcp_bridge_proto_rawDescGZIP(), []int{3}
}
func (x *InnerMap) GetInnerMap() map[string]string {
if x != nil {
return x.InnerMap
}
return nil
}
var File_networking_v1_mcp_bridge_proto protoreflect.FileDescriptor
var file_networking_v1_mcp_bridge_proto_rawDesc = []byte{
@@ -303,61 +503,123 @@ var file_networking_v1_mcp_bridge_proto_rawDesc = []byte{
0x12, 0x15, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f,
0x61, 0x70, 0x69, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69,
0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x52, 0x0a, 0x09, 0x4d, 0x63, 0x70, 0x42,
0x72, 0x69, 0x64, 0x67, 0x65, 0x12, 0x45, 0x0a, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72,
0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x68, 0x69, 0x67, 0x72,
0x65, 0x73, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76,
0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x52, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0xd3, 0x05, 0x0a,
0x0e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
0x17, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0,
0x41, 0x02, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x06,
0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41,
0x02, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x17, 0x0a, 0x04, 0x70, 0x6f, 0x72,
0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x04, 0x70, 0x6f,
0x72, 0x74, 0x12, 0x2e, 0x0a, 0x12, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x41, 0x64, 0x64, 0x72, 0x65,
0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12,
0x6e, 0x61, 0x63, 0x6f, 0x73, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76,
0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x41, 0x63, 0x63, 0x65, 0x73,
0x73, 0x4b, 0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x61, 0x63, 0x6f,
0x73, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x61,
0x63, 0x6f, 0x73, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b,
0x65, 0x79, 0x12, 0x2a, 0x0a, 0x10, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73,
0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6e, 0x61,
0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x26,
0x0a, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65,
0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d,
0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x47,
0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x61, 0x63,
0x6f, 0x73, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x6e, 0x61, 0x63, 0x6f,
0x73, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c,
0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x52, 0x65, 0x66,
0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x28, 0x0a, 0x0f,
0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18,
0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x4e, 0x61, 0x6d,
0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x7a, 0x6b, 0x53, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x73, 0x50, 0x61, 0x74, 0x68, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e,
0x7a, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x50, 0x61, 0x74, 0x68, 0x12, 0x2a,
0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74,
0x65, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c,
0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x10, 0x63, 0x6f,
0x6e, 0x73, 0x75, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x54, 0x61, 0x67, 0x18, 0x0f,
0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x53, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x54, 0x61, 0x67, 0x12, 0x34, 0x0a, 0x15, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c,
0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18,
0x10, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x52, 0x65, 0x66,
0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x26, 0x0a, 0x0e,
0x61, 0x75, 0x74, 0x68, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x11,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74,
0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c,
0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c,
0x12, 0x10, 0x0a, 0x03, 0x73, 0x6e, 0x69, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x73,
0x6e, 0x69, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x2f, 0x61, 0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x2f, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73,
0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2f,
0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65,
0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x90, 0x01, 0x0a, 0x09, 0x4d, 0x63, 0x70, 0x42, 0x72,
0x69, 0x64, 0x67, 0x65, 0x12, 0x45, 0x0a, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x69,
0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65,
0x73, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31,
0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x3c, 0x0a, 0x07, 0x70,
0x72, 0x6f, 0x78, 0x69, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x68,
0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e,
0x67, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x52, 0x07, 0x70, 0x72, 0x6f, 0x78, 0x69, 0x65, 0x73, 0x22, 0xc6, 0x09, 0x0a, 0x0e, 0x52, 0x65,
0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x17, 0x0a, 0x04,
0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52,
0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x06, 0x64, 0x6f, 0x6d,
0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x06,
0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x17, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04,
0x20, 0x01, 0x28, 0x0d, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12,
0x2e, 0x0a, 0x12, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53,
0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x6e, 0x61, 0x63,
0x6f, 0x73, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12,
0x26, 0x0a, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65,
0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x41, 0x63,
0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73,
0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x12,
0x2a, 0x0a, 0x10, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63,
0x65, 0x49, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6e, 0x61, 0x63, 0x6f, 0x73,
0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x6e,
0x61, 0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x09, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70,
0x61, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x47, 0x72, 0x6f, 0x75,
0x70, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x47,
0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x52, 0x65,
0x66, 0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x0b, 0x20,
0x01, 0x28, 0x03, 0x52, 0x14, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73,
0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x28, 0x0a, 0x0f, 0x63, 0x6f, 0x6e,
0x73, 0x75, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x0c, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70,
0x61, 0x63, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x7a, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x73, 0x50, 0x61, 0x74, 0x68, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x7a, 0x6b, 0x53,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x50, 0x61, 0x74, 0x68, 0x12, 0x2a, 0x0a, 0x10, 0x63,
0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x18,
0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x44, 0x61, 0x74,
0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x75,
0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x54, 0x61, 0x67, 0x18, 0x0f, 0x20, 0x01, 0x28,
0x09, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x54, 0x61, 0x67, 0x12, 0x34, 0x0a, 0x15, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x52, 0x65, 0x66,
0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x10, 0x20, 0x01,
0x28, 0x03, 0x52, 0x15, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73,
0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x26, 0x0a, 0x0e, 0x61, 0x75, 0x74,
0x68, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x11, 0x20, 0x01, 0x28,
0x09, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4e, 0x61, 0x6d,
0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x12, 0x20,
0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x10, 0x0a,
0x03, 0x73, 0x6e, 0x69, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x73, 0x6e, 0x69, 0x12,
0x36, 0x0a, 0x16, 0x6d, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x78, 0x70, 0x6f,
0x72, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x14, 0x20, 0x03, 0x28, 0x09, 0x52,
0x16, 0x6d, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74,
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x6d, 0x63, 0x70, 0x53, 0x65,
0x72, 0x76, 0x65, 0x72, 0x42, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x18, 0x15, 0x20, 0x01, 0x28,
0x09, 0x52, 0x10, 0x6d, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x42, 0x61, 0x73, 0x65,
0x55, 0x72, 0x6c, 0x12, 0x44, 0x0a, 0x0f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x43, 0x50,
0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x16, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42,
0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65,
0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x50, 0x0a, 0x15, 0x65, 0x6e, 0x61,
0x62, 0x6c, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x4d, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76, 0x65,
0x72, 0x73, 0x18, 0x17, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56,
0x61, 0x6c, 0x75, 0x65, 0x52, 0x15, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x63, 0x6f, 0x70,
0x65, 0x4d, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x61,
0x6c, 0x6c, 0x6f, 0x77, 0x4d, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x18,
0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x4d, 0x63, 0x70, 0x53, 0x65,
0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x4f, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
0x61, 0x18, 0x19, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73,
0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e,
0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x4d,
0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65,
0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x4e,
0x61, 0x6d, 0x65, 0x18, 0x1a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x78, 0x79,
0x4e, 0x61, 0x6d, 0x65, 0x1a, 0x5c, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x35, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73,
0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x49,
0x6e, 0x6e, 0x65, 0x72, 0x4d, 0x61, 0x70, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02,
0x38, 0x01, 0x22, 0xdb, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x43, 0x6f, 0x6e, 0x66,
0x69, 0x67, 0x12, 0x17, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x17, 0x0a, 0x04, 0x6e,
0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x04,
0x6e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x0d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64,
0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02,
0x52, 0x0d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12,
0x23, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20,
0x01, 0x28, 0x0d, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72,
0x50, 0x6f, 0x72, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72,
0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x6c, 0x69, 0x73, 0x74,
0x65, 0x6e, 0x65, 0x72, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x6e,
0x65, 0x63, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d,
0x52, 0x0e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74,
0x22, 0x93, 0x01, 0x0a, 0x08, 0x49, 0x6e, 0x6e, 0x65, 0x72, 0x4d, 0x61, 0x70, 0x12, 0x4a, 0x0a,
0x09, 0x69, 0x6e, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
0x32, 0x2d, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f,
0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x6e, 0x65, 0x72, 0x4d, 0x61,
0x70, 0x2e, 0x49, 0x6e, 0x6e, 0x65, 0x72, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52,
0x08, 0x69, 0x6e, 0x6e, 0x65, 0x72, 0x4d, 0x61, 0x70, 0x1a, 0x3b, 0x0a, 0x0d, 0x49, 0x6e, 0x6e,
0x65, 0x72, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65,
0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05,
0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c,
0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x2f, 0x68, 0x69, 0x67,
0x72, 0x65, 0x73, 0x73, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
0x69, 0x6e, 0x67, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -372,18 +634,29 @@ func file_networking_v1_mcp_bridge_proto_rawDescGZIP() []byte {
return file_networking_v1_mcp_bridge_proto_rawDescData
}
var file_networking_v1_mcp_bridge_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_networking_v1_mcp_bridge_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_networking_v1_mcp_bridge_proto_goTypes = []interface{}{
(*McpBridge)(nil), // 0: higress.networking.v1.McpBridge
(*RegistryConfig)(nil), // 1: higress.networking.v1.RegistryConfig
(*McpBridge)(nil), // 0: higress.networking.v1.McpBridge
(*RegistryConfig)(nil), // 1: higress.networking.v1.RegistryConfig
(*ProxyConfig)(nil), // 2: higress.networking.v1.ProxyConfig
(*InnerMap)(nil), // 3: higress.networking.v1.InnerMap
nil, // 4: higress.networking.v1.RegistryConfig.MetadataEntry
nil, // 5: higress.networking.v1.InnerMap.InnerMapEntry
(*wrappers.BoolValue)(nil), // 6: google.protobuf.BoolValue
}
var file_networking_v1_mcp_bridge_proto_depIdxs = []int32{
1, // 0: higress.networking.v1.McpBridge.registries:type_name -> higress.networking.v1.RegistryConfig
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
2, // 1: higress.networking.v1.McpBridge.proxies:type_name -> higress.networking.v1.ProxyConfig
6, // 2: higress.networking.v1.RegistryConfig.enableMCPServer:type_name -> google.protobuf.BoolValue
6, // 3: higress.networking.v1.RegistryConfig.enableScopeMcpServers:type_name -> google.protobuf.BoolValue
4, // 4: higress.networking.v1.RegistryConfig.metadata:type_name -> higress.networking.v1.RegistryConfig.MetadataEntry
5, // 5: higress.networking.v1.InnerMap.inner_map:type_name -> higress.networking.v1.InnerMap.InnerMapEntry
3, // 6: higress.networking.v1.RegistryConfig.MetadataEntry.value:type_name -> higress.networking.v1.InnerMap
7, // [7:7] is the sub-list for method output_type
7, // [7:7] is the sub-list for method input_type
7, // [7:7] is the sub-list for extension type_name
7, // [7:7] is the sub-list for extension extendee
0, // [0:7] is the sub-list for field type_name
}
func init() { file_networking_v1_mcp_bridge_proto_init() }
@@ -416,6 +689,30 @@ func file_networking_v1_mcp_bridge_proto_init() {
return nil
}
}
file_networking_v1_mcp_bridge_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ProxyConfig); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_networking_v1_mcp_bridge_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*InnerMap); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
@@ -423,7 +720,7 @@ func file_networking_v1_mcp_bridge_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_networking_v1_mcp_bridge_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumMessages: 6,
NumExtensions: 0,
NumServices: 0,
},

View File

@@ -15,6 +15,8 @@
syntax = "proto3";
import "google/api/field_behavior.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/struct.proto";
// $schema: higress.networking.v1.McpBridge
// $title: McpBridge
@@ -44,6 +46,7 @@ option go_package = "github.com/alibaba/higress/api/networking/v1";
// -->
message McpBridge {
repeated RegistryConfig registries = 1;
repeated ProxyConfig proxies = 2;
}
message RegistryConfig {
@@ -66,4 +69,24 @@ message RegistryConfig {
string authSecretName = 17;
string protocol = 18;
string sni = 19;
repeated string mcpServerExportDomains = 20;
string mcpServerBaseUrl = 21;
google.protobuf.BoolValue enableMCPServer = 22;
google.protobuf.BoolValue enableScopeMcpServers = 23;
repeated string allowMcpServers = 24;
map<string, InnerMap> metadata = 25;
string proxyName = 26;
}
message ProxyConfig {
string type = 1 [(google.api.field_behavior) = REQUIRED];
string name = 2 [(google.api.field_behavior) = REQUIRED];
string serverAddress = 3 [(google.api.field_behavior) = REQUIRED];
uint32 serverPort = 4 [(google.api.field_behavior) = REQUIRED];
uint32 listenerPort = 5;
uint32 connectTimeout = 6;
}
message InnerMap {
map<string, string> inner_map = 1;
}

View File

@@ -46,3 +46,45 @@ func (in *RegistryConfig) DeepCopy() *RegistryConfig {
func (in *RegistryConfig) DeepCopyInterface() interface{} {
return in.DeepCopy()
}
// DeepCopyInto supports using ProxyConfig within kubernetes types, where deepcopy-gen is used.
func (in *ProxyConfig) DeepCopyInto(out *ProxyConfig) {
p := proto.Clone(in).(*ProxyConfig)
*out = *p
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyConfig. Required by controller-gen.
func (in *ProxyConfig) DeepCopy() *ProxyConfig {
if in == nil {
return nil
}
out := new(ProxyConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInterface is an autogenerated deepcopy function, copying the receiver, creating a new ProxyConfig. Required by controller-gen.
func (in *ProxyConfig) DeepCopyInterface() interface{} {
return in.DeepCopy()
}
// DeepCopyInto supports using InnerMap within kubernetes types, where deepcopy-gen is used.
func (in *InnerMap) DeepCopyInto(out *InnerMap) {
p := proto.Clone(in).(*InnerMap)
*out = *p
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InnerMap. Required by controller-gen.
func (in *InnerMap) DeepCopy() *InnerMap {
if in == nil {
return nil
}
out := new(InnerMap)
in.DeepCopyInto(out)
return out
}
// DeepCopyInterface is an autogenerated deepcopy function, copying the receiver, creating a new InnerMap. Required by controller-gen.
func (in *InnerMap) DeepCopyInterface() interface{} {
return in.DeepCopy()
}

View File

@@ -28,6 +28,28 @@ func (this *RegistryConfig) UnmarshalJSON(b []byte) error {
return McpBridgeUnmarshaler.Unmarshal(bytes.NewReader(b), this)
}
// MarshalJSON is a custom marshaler for ProxyConfig
func (this *ProxyConfig) MarshalJSON() ([]byte, error) {
str, err := McpBridgeMarshaler.MarshalToString(this)
return []byte(str), err
}
// UnmarshalJSON is a custom unmarshaler for ProxyConfig
func (this *ProxyConfig) UnmarshalJSON(b []byte) error {
return McpBridgeUnmarshaler.Unmarshal(bytes.NewReader(b), this)
}
// MarshalJSON is a custom marshaler for InnerMap
func (this *InnerMap) MarshalJSON() ([]byte, error) {
str, err := McpBridgeMarshaler.MarshalToString(this)
return []byte(str), err
}
// UnmarshalJSON is a custom unmarshaler for InnerMap
func (this *InnerMap) UnmarshalJSON(b []byte) error {
return McpBridgeUnmarshaler.Unmarshal(bytes.NewReader(b), this)
}
var (
McpBridgeMarshaler = &jsonpb.Marshaler{}
McpBridgeUnmarshaler = &jsonpb.Unmarshaler{AllowUnknownFields: true}

View File

@@ -95,6 +95,6 @@ generate-k8s-client:
.PHONY: clean-k8s-client
clean-k8s-cliennt:
clean-k8s-client:
# remove generated code
@rm -rf pkg/

View File

@@ -6,11 +6,11 @@ ARG BASE_VERSION=latest
ARG HUB
ARG TARGETARCH
# The following section is used as base image if BASE_DISTRIBUTION=debug
# This base image is provided by istio, see: https://github.com/istio/istio/blob/master/docker/Dockerfile.base
FROM ${HUB}/base:${BASE_VERSION}
ARG TARGETARCH
FROM ${HUB}/base:${BASE_VERSION}-${TARGETARCH}
COPY ${TARGETARCH:-amd64}/higress /usr/local/bin/higress

View File

@@ -17,6 +17,11 @@ docker.higress: $(OUT_LINUX)/higress
docker.higress: docker/Dockerfile.higress
$(HIGRESS_DOCKER_RULE)
docker.higress-amd64: BUILD_ARGS=--build-arg BASE_VERSION=${HIGRESS_BASE_VERSION} --build-arg HUB=${HUB}
docker.higress-amd64: $(AMD64_OUT_LINUX)/higress
docker.higress-amd64: docker/Dockerfile.higress
$(HIGRESS_DOCKER_AMD64_RULE)
docker.higress-buildx: BUILD_ARGS=--build-arg BASE_VERSION=${HIGRESS_BASE_VERSION} --build-arg HUB=${HUB}
docker.higress-buildx: $(AMD64_OUT_LINUX)/higress
docker.higress-buildx: $(ARM64_OUT_LINUX)/higress
@@ -40,3 +45,4 @@ 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 . ); )
HIGRESS_DOCKER_AMD64_RULE ?= $(foreach VARIANT,$(DOCKER_BUILD_VARIANTS), time (mkdir -p $(HIGRESS_DOCKER_BUILD_TOP)/$@ && TARGET_ARCH=amd64 ./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)) --build-arg TARGETARCH=amd64 -t $(IMG_URL)$(call variant-tag,$(VARIANT)) -f Dockerfile.higress . ); )

41
go.mod
View File

@@ -31,7 +31,7 @@ require (
github.com/hudl/fargo v1.4.0
github.com/mholt/acmez v1.2.0
github.com/nacos-group/nacos-sdk-go v1.0.8
github.com/nacos-group/nacos-sdk-go/v2 v2.1.2
github.com/nacos-group/nacos-sdk-go/v2 v2.3.2
github.com/onsi/gomega v1.27.10
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5
@@ -39,7 +39,7 @@ require (
github.com/tidwall/gjson v1.17.0
go.uber.org/atomic v1.11.0
go.uber.org/zap v1.27.0
golang.org/x/net v0.27.0
golang.org/x/net v0.33.0
google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13
google.golang.org/grpc v1.59.0
google.golang.org/protobuf v1.33.0
@@ -71,7 +71,27 @@ require (
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/alecholmes/xfccparser v0.1.0 // indirect
github.com/alecthomas/participle v0.4.1 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704 // indirect
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/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
@@ -82,10 +102,12 @@ require (
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/clbanning/mxj v1.8.4 // indirect
github.com/clbanning/mxj/v2 v2.5.5 // indirect
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
github.com/coreos/go-oidc/v3 v3.6.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/deckarep/golang-set v1.7.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/docker/cli v24.0.7+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
@@ -165,6 +187,7 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
github.com/openshift/api v0.0.0-20230720094506-afcbe27aec7c // indirect
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
@@ -179,9 +202,11 @@ require (
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/tetratelabs/wazero v1.7.3 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/toolkits/concurrent v0.0.0-20150624120057-a4371d70e3e3 // indirect
github.com/vbatts/tar-split v0.11.3 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
@@ -197,14 +222,14 @@ require (
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/oauth2 v0.13.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/term v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect

144
go.sum
View File

@@ -683,9 +683,68 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
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/alessio/shellescape v1.2.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
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.18/go.mod h1:v8ESoHo4SyHmuB4b1tJqDHxfTGEciD+yhvOU/5s1Rfk=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704 h1:PpfENOj/vPfhhy9N2OFRjpue0hjM5XqAp2thFmkXXIk=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU=
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/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@@ -755,7 +814,6 @@ github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6
github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
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.11-0.20170329064859-445be9e134b2/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@@ -765,6 +823,8 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
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/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
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=
@@ -813,6 +873,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
@@ -1162,8 +1224,9 @@ github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97Dwqy
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
@@ -1460,8 +1523,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/nacos-group/nacos-sdk-go v1.0.8 h1:8pEm05Cdav9sQgJSv5kyvlgfz0SzFUUGI3pWX6SiSnM=
github.com/nacos-group/nacos-sdk-go v1.0.8/go.mod h1:hlAPn3UdzlxIlSILAyOXKxjFSvDJ9oLzTJ9hLAK1KzA=
github.com/nacos-group/nacos-sdk-go/v2 v2.1.2 h1:A8GV6j0rw80I6tTKSav/pTpEgNECYXeFvZCsiLBWGnQ=
github.com/nacos-group/nacos-sdk-go/v2 v2.1.2/go.mod h1:ys/1adWeKXXzbNWfRNbaFlX/t6HVLWdpsNDvmoWTw0g=
github.com/nacos-group/nacos-sdk-go/v2 v2.3.2 h1:9QB2nCJzT5wkTVlxNYl3XL/7+G6p2USMi2gQh/ouQQo=
github.com/nacos-group/nacos-sdk-go/v2 v2.3.2/go.mod h1:9FKXl6FqOiVmm72i8kADtbeK71egyG9y3uRDBg41tpQ=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
@@ -1517,6 +1580,8 @@ github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJ
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.3.0/go.mod h1:4c3sLeE8xjNqehmF5RpAFLPLJxXscc0R4l6Zg0P1tTQ=
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/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
@@ -1560,7 +1625,6 @@ github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3O
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
@@ -1593,7 +1657,6 @@ github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.2.0/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.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/prometheus/prometheus v0.45.0 h1:O/uG+Nw4kNxx/jDPxmjsSDd+9Ohql6E7ZSY1x5x/0KI=
@@ -1643,8 +1706,9 @@ github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
@@ -1713,6 +1777,9 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
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/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@@ -1746,6 +1813,7 @@ github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+Seva
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
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=
@@ -1832,7 +1900,6 @@ go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0
go.uber.org/automaxprocs v1.4.0/go.mod h1:/mTEdr7LvHhs0v7mjdxDreTz1OG5zdZGqgOnhWiR/+Q=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
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.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
@@ -1849,7 +1916,6 @@ go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
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=
@@ -1868,9 +1934,12 @@ golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
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-20200220183623-bac4c82f6975/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-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
@@ -1882,8 +1951,13 @@ golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
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-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
@@ -1970,6 +2044,7 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
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-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
@@ -2008,8 +2083,13 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
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=
@@ -2059,8 +2139,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -2107,6 +2187,7 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/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-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -2156,7 +2237,6 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/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-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -2181,8 +2261,13 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -2195,8 +2280,13 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
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/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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=
@@ -2214,8 +2304,11 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
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-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
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=
@@ -2225,7 +2318,6 @@ golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
@@ -2279,6 +2371,7 @@ golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjs
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-20200505023115-26f46d2f7ef8/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=
@@ -2646,6 +2739,7 @@ gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
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=

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 2.1.1
appVersion: 2.1.7
description: Helm chart for deploying higress gateways
icon: https://higress.io/img/higress_logo_small.png
home: http://higress.io/
@@ -15,4 +15,4 @@ dependencies:
repository: "file://../redis"
version: 0.0.1
type: application
version: 2.1.1
version: 2.1.7

View File

@@ -247,9 +247,30 @@ spec:
properties:
spec:
properties:
proxies:
items:
properties:
connectTimeout:
type: integer
listenerPort:
type: integer
name:
type: string
serverAddress:
type: string
serverPort:
type: integer
type:
type: string
type: object
type: array
registries:
items:
properties:
allowMcpServers:
items:
type: string
type: array
authSecretName:
type: string
consulDatacenter:
@@ -263,6 +284,25 @@ spec:
type: string
domain:
type: string
enableMCPServer:
type: boolean
enableScopeMcpServers:
type: boolean
mcpServerBaseUrl:
type: string
mcpServerExportDomains:
items:
type: string
type: array
metadata:
additionalProperties:
properties:
innerMap:
additionalProperties:
type: string
type: object
type: object
type: object
nacosAccessKey:
type: string
nacosAddressServer:
@@ -286,6 +326,8 @@ spec:
type: integer
protocol:
type: string
proxyName:
type: string
sni:
type: string
type:

View File

@@ -113,3 +113,36 @@ kind: VMPodScrape
{{- fail "unexpected gateway.metrics.provider" -}}
{{- end -}}
{{- end -}}
{{- define "pluginServer.name" -}}
{{- .Values.pluginServer.name | default "higress-plugin-server" -}}
{{- end }}
{{- define "pluginServer.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "pluginServer.labels" -}}
helm.sh/chart: {{ include "pluginServer.chart" . }}
{{ include "pluginServer.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/name: {{ include "pluginServer.name" . }}
{{- end }}
{{- define "pluginServer.selectorLabels" -}}
{{- if hasKey .Values.pluginServer.labels "app" }}
{{- with .Values.pluginServer.labels.app }}app: {{.|quote}}
{{- end}}
{{- else }}app: {{ include "pluginServer.name" . }}
{{- end }}
{{- if hasKey .Values.pluginServer.labels "higress" }}
{{- with .Values.pluginServer.labels.higress }}
higress: {{.|quote}}
{{- end}}
{{- else }}
higress: {{ include "pluginServer.name" . }}
{{- end }}
{{- end }}

View File

@@ -9,9 +9,7 @@
accessLogFile: "/dev/stdout"
{{- end }}
ingressControllerMode: "OFF"
accessLogFormat: '{"ai_log":"%FILTER_STATE(wasm.ai_log:PLAIN)%","authority":"%REQ(X-ENVOY-ORIGINAL-HOST?:AUTHORITY)%","bytes_received":"%BYTES_RECEIVED%","bytes_sent":"%BYTES_SENT%","downstream_local_address":"%DOWNSTREAM_LOCAL_ADDRESS%","downstream_remote_address":"%DOWNSTREAM_REMOTE_ADDRESS%","duration":"%DURATION%","istio_policy_status":"%DYNAMIC_METADATA(istio.mixer:status)%","method":"%REQ(:METHOD)%","path":"%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%","protocol":"%PROTOCOL%","request_id":"%REQ(X-REQUEST-ID)%","requested_server_name":"%REQUESTED_SERVER_NAME%","response_code":"%RESPONSE_CODE%","response_flags":"%RESPONSE_FLAGS%","route_name":"%ROUTE_NAME%","start_time":"%START_TIME%","trace_id":"%REQ(X-B3-TRACEID)%","upstream_cluster":"%UPSTREAM_CLUSTER%","upstream_host":"%UPSTREAM_HOST%","upstream_local_address":"%UPSTREAM_LOCAL_ADDRESS%","upstream_service_time":"%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%","upstream_transport_failure_reason":"%UPSTREAM_TRANSPORT_FAILURE_REASON%","user_agent":"%REQ(USER-AGENT)%","x_forwarded_for":"%REQ(X-FORWARDED-FOR)%","response_code_details":"%RESPONSE_CODE_DETAILS%"}
'
accessLogFormat: '{"ai_log":"%FILTER_STATE(wasm.ai_log:PLAIN)%","authority":"%REQ(X-ENVOY-ORIGINAL-HOST?:AUTHORITY)%","bytes_received":"%BYTES_RECEIVED%","bytes_sent":"%BYTES_SENT%","downstream_local_address":"%DOWNSTREAM_LOCAL_ADDRESS%","downstream_remote_address":"%DOWNSTREAM_REMOTE_ADDRESS%","duration":"%DURATION%","istio_policy_status":"%DYNAMIC_METADATA(istio.mixer:status)%","method":"%REQ(:METHOD)%","path":"%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%","protocol":"%PROTOCOL%","request_id":"%REQ(X-REQUEST-ID)%","requested_server_name":"%REQUESTED_SERVER_NAME%","response_code":"%RESPONSE_CODE%","response_flags":"%RESPONSE_FLAGS%","route_name":"%ROUTE_NAME%","start_time":"%START_TIME%","trace_id":"%REQ(X-B3-TRACEID)%","upstream_cluster":"%UPSTREAM_CLUSTER%","upstream_host":"%UPSTREAM_HOST%","upstream_local_address":"%UPSTREAM_LOCAL_ADDRESS%","upstream_service_time":"%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%","upstream_transport_failure_reason":"%UPSTREAM_TRANSPORT_FAILURE_REASON%","user_agent":"%REQ(USER-AGENT)%","x_forwarded_for":"%REQ(X-FORWARDED-FOR)%","response_code_details":"%RESPONSE_CODE_DETAILS%"}'
dnsRefreshRate: 200s
enableAutoMtls: false
enablePrometheusMerge: false
@@ -99,7 +97,7 @@ metadata:
name: higress-config
namespace: {{ .Release.Namespace }}
labels:
{{- include "gateway.labels" . | nindent 4 }}
{{- include "gateway.labels" . | nindent 4 }}
data:
higress: |-
{{- $existingConfig := lookup "v1" "ConfigMap" .Release.Namespace "higress-config" }}
@@ -126,7 +124,7 @@ data:
{{- else }}
networks: {}
{{- end }}
mesh: |-
{{- if .Values.meshConfig }}
{{ $mesh | toYaml | indent 4 }}

View File

@@ -6,4 +6,8 @@ metadata:
namespace: {{ .Release.Namespace }}
labels:
{{- include "controller.labels" . | nindent 4 }}
{{- with .Values.controller.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,39 @@
{{- if .Values.global.enablePluginServer }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "pluginServer.name" . }}
namespace: {{ .Release.Namespace }}
spec:
replicas: {{ .Values.pluginServer.replicas }}
selector:
matchLabels:
{{- include "pluginServer.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- with .Values.pluginServer.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- include "pluginServer.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.pluginServer.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: {{ .Values.pluginServer.hub | default .Values.global.hub }}/{{ .Values.pluginServer.image | default "plugin-server" }}:{{ .Values.pluginServer.tag | default "1.0.0" }}
{{- if .Values.global.imagePullPolicy }}
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
{{- end }}
ports:
- containerPort: 8080
resources:
requests:
cpu: {{ .Values.pluginServer.resources.requests.cpu }}
memory: {{ .Values.pluginServer.resources.requests.memory }}
limits:
cpu: {{ .Values.pluginServer.resources.limits.cpu }}
memory: {{ .Values.pluginServer.resources.limits.memory }}
{{- end }}

View File

@@ -0,0 +1,16 @@
{{- if .Values.global.enablePluginServer }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "pluginServer.name" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "pluginServer.labels" . | nindent 4 }}
spec:
ports:
- protocol: TCP
port: {{ .Values.pluginServer.service.port }}
targetPort: 8080
selector:
{{- include "pluginServer.selectorLabels" . | nindent 4 }}
{{- end }}

View File

@@ -11,6 +11,7 @@ global:
enableSRDS: true
# -- Whether to enable Redis(redis-stack-server) for Higress, default is false.
enableRedis: false
enablePluginServer: false
onDemandRDS: false
hostRDSMergeSubset: false
onlyPushRouteCluster: true
@@ -580,8 +581,7 @@ controller:
# -- Labels to apply to the pod
podLabels: {}
podSecurityContext:
{}
podSecurityContext: {}
# fsGroup: 2000
ports:
@@ -708,13 +708,13 @@ tracing:
enable: false
sampling: 100
timeout: 500
skywalking:
# access_token: ""
service: ""
port: 11800
# skywalking:
# access_token: ""
# service: ""
# port: 11800
# zipkin:
# service: ""
# port: 9411
# service: ""
# port: 9411
# -- Downstream config settings
downstream:
@@ -767,4 +767,31 @@ redis:
accessModes:
- ReadWriteOnce
# -- Persistent Volume size
size: 1Gi
size: 1Gi
pluginServer:
name: "higress-plugin-server"
# -- Number of Higress Plugin Server pods, 2 recommended for high availability
replicas: 2
image: plugin-server
hub: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress
tag: ""
imagePullSecrets: []
labels: {}
# -- Labels to apply to the pod
podLabels: {}
# Plugin-server Service configuration
service:
port: 80 # Container target port (usually fixed)
resources:
requests:
cpu: 200m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi

View File

@@ -1,9 +1,9 @@
dependencies:
- name: higress-core
repository: file://../core
version: 2.1.1
version: 2.1.7
- name: higress-console
repository: https://higress.io/helm-charts/
version: 2.1.1
digest: sha256:a40f56252d0aa995fbf62952a6d23304dd84845caf5766bc64f7270cb00f01e9
generated: "2025-04-18T16:45:23.961507+08:00"
version: 2.1.7
digest: sha256:c5bc8ddcc56c66751217aee5c7a40da0a906bfa9fc5c671cc4ae6e456db6bc21
generated: "2025-09-01T15:19:26.228634+08:00"

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 2.1.1
appVersion: 2.1.7
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.1.1
version: 2.1.7
- name: higress-console
repository: "https://higress.io/helm-charts/"
version: 2.1.1
version: 2.1.7
type: application
version: 2.1.1
version: 2.1.7

View File

@@ -165,6 +165,7 @@ The command removes all the Kubernetes components associated with the chart and
| global.enableIPv6 | bool | `false` | |
| global.enableIstioAPI | bool | `true` | If true, Higress Controller will monitor istio resources as well |
| global.enableLDSCache | bool | `false` | |
| global.enablePluginServer | 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. |
@@ -273,6 +274,19 @@ The command removes all the Kubernetes components associated with the chart and
| pilot.serviceAnnotations | object | `{}` | |
| pilot.tag | string | `""` | |
| pilot.traceSampling | float | `1` | |
| pluginServer.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
| pluginServer.image | string | `"plugin-server"` | |
| pluginServer.imagePullSecrets | list | `[]` | |
| pluginServer.labels | object | `{}` | |
| pluginServer.name | string | `"higress-plugin-server"` | |
| pluginServer.podLabels | object | `{}` | Labels to apply to the pod |
| pluginServer.replicas | int | `2` | Number of Higress Plugin Server pods, 2 recommended for high availability |
| pluginServer.resources.limits.cpu | string | `"500m"` | |
| pluginServer.resources.limits.memory | string | `"256Mi"` | |
| pluginServer.resources.requests.cpu | string | `"200m"` | |
| pluginServer.resources.requests.memory | string | `"128Mi"` | |
| pluginServer.service.port | int | `80` | |
| pluginServer.tag | string | `""` | |
| redis.redis.affinity | object | `{}` | Affinity for Redis |
| redis.redis.image | string | `"redis-stack-server"` | Specify the image |
| redis.redis.name | string | `"redis-stack-server"` | |
@@ -292,7 +306,5 @@ The command removes all the Kubernetes components associated with the chart and
| revision | string | `""` | |
| tracing.enable | bool | `false` | |
| tracing.sampling | int | `100` | |
| tracing.skywalking.port | int | `11800` | |
| tracing.skywalking.service | string | `""` | |
| tracing.timeout | int | `500` | |
| upstream | object | `{"connectionBufferLimits":10485760,"idleTimeout":10}` | Upstream config settings |

View File

@@ -1,8 +1,8 @@
## Higress for Kubernetes
## Higress 适用于 Kubernetes
Higress 是基于阿里巴巴内部网关实践构建的云原生 API 网关。
Higress 是基于阿里巴巴内部网关实践的云原生 API 网关。
依托 Istio 和 EnvoyHigress 实现了流量网关、微服务网关和安全网关三架构的融合,从而大幅降低了部署、运维成本。
通过 Istio 和 Envoy 的支持Higress 实现了流量网关、微服务网关和安全网关三架构的融合,从而极大地减少了部署、运维成本。
## 设置仓库信息
@@ -13,7 +13,7 @@ helm repo update
## 安装
`higress` 为发布名称安装 chart
使用 Helm 安装名为 `higress` 的组件
```console
helm install higress -n higress-system higress.io/higress --create-namespace --render-subchart-notes
@@ -21,168 +21,130 @@ helm install higress -n higress-system higress.io/higress --create-namespace --r
## 卸载
要卸载/删除 higress 部署
删除名称为 higress 的安装
```console
helm delete higress -n higress-system
```
该命令会移除与 chart 相关的所有 Kubernetes 组件,并删除发布
该命令将删除与组件关联的所有 Kubernetes 组件并卸载该发行版
## 参数
##
## Values
| 键 | 类型 | 默认值 | 描述 |
|-----|------|---------|-------------|
| 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 无法直接相互通信时。
|----|------|---------|-------------|
| clusterName | string | `""` | 集群名 |
| controller.affinity | object | `{}` | 控制器亲和性设置 |
| controller.automaticHttps.email | string | `""` | 自动 HTTPS 所需的邮件 |
| controller.automaticHttps.enabled | bool | `true` | 是否启用自动 HTTPS 功能 |
| controller.autoscaling.enabled | bool | `false` | 是否启用控制器的自动扩展功能 |
| controller.autoscaling.maxReplicas | int | `5` | 最大副本数 |
| controller.autoscaling.minReplicas | int | `1` | 最小副本数 |
| controller.autoscaling.targetCPUUtilizationPercentage | int | `80` | 目标 CPU 使用率百分比 |
| controller.env | object | `{}` | 环境变量 |
| controller.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | 图像库的基础地址 |
| controller.image | string | `"higress"` | 镜像名称 |
| controller.imagePullSecrets | list | `[]` | 拉取秘钥列表 |
| controller.labels | object | `{}` | 标签 |
| controller.name | string | `"higress-controller"` | 控制器名称 |
| controller.nodeSelector | object | `{}` | 节点选择器 |
| controller.podAnnotations | object | `{}` | Pod 注解 |
| controller.podLabels | object | `{}` | 应用到 Pod 上的标签 |
| controller.podSecurityContext | object | `{}` | Pod 安全上下文 |
| controller.ports[0].name | string | `"http"` | 端口名称 |
| controller.ports[0].port | int | `8888` | 端口编号 |
| controller.ports[0].protocol | string | `"TCP"` | 协议类型 |
| controller.ports[0].targetPort | int | `8888` | 目标端口 |
| controller.ports[1].name | string | `"http-solver"` | 端口名称 |
| controller.ports[1].port | int | `8889` | 端口编号 |
| controller.ports[1].protocol | string | `"TCP"` | 协议类型 |
| controller.ports[1].targetPort | int | `8889` | 目标端口 |
| controller.ports[2].name | string | `"grpc"` | 端口名称 |
| controller.ports[2].port | int | `15051` | 端口编号 |
| controller.ports[2].protocol | string | `"TCP"` | 协议类型 |
| controller.ports[2].targetPort | int | `15051` | 目标端口 |
| controller.probe.httpGet.path | string | `"/ready"` | 运行状况检查路径 |
| controller.probe.httpGet.port | int | `8888` | 端口运行状态检查 |
| controller.probe.initialDelaySeconds | int | `1` | 初始延迟秒数 |
| controller.probe.periodSeconds | int | `3` | 健康检查间隔秒数 |
| controller.probe.timeoutSeconds | int | `5` | 超时秒数 |
| controller.rbac.create | bool | `true` | 是否创建 RBAC 相关资源 |
| controller.replicas | int | `1` | Higress 控制器 Pod 的数量 |
| controller.resources.limits.cpu | string | `"1000m"` | CPU 上限 |
| controller.resources.limits.memory | string | `"2048Mi"` | 内存上限 |
| controller.resources.requests.cpu | string | `"500m"` | CPU 请求量 |
| controller.resources.requests.memory | string | `"2048Mi"` | 内存请求量 |
| controller.securityContext | object | `{}` | 安全上下文 |
| controller.service.type | string | `"ClusterIP"` | 服务类型 |
| controller.serviceAccount.annotations | object | `{}` | 添加到服务帐户的注解 |
| controller.serviceAccount.create | bool | `true` | 是否创建服务帐户 |
| controller.serviceAccount.name | string | `""` | 如果未设置且 create 为 true则从 fullname 模板生成名称 |
| controller.tag | string | `""` | 标记 |
| controller.tolerations | list | `[]` | 受容容忍度列表 |
| downstream.connectionBufferLimits | int | `32768` | 下游连接缓冲区限制(字节) |
| downstream.http2.initialConnectionWindowSize | int | `1048576` | HTTP/2 初始连接窗口大小 |
| downstream.http2.initialStreamWindowSize | int | `65535` | 流初始窗口大小 |
| downstream.http2.maxConcurrentStreams | int | `100` | 并发流最大数量 |
| downstream.idleTimeout | int | `180` | 空闲超时时间(秒) |
| downstream.maxRequestHeadersKb | int | `60` | 最大请求头大小KB |
| downstream.routeTimeout | int | `0` | 路由超时时间 |
| gateway.affinity | object | `{}` | 网关的节点亲和性 |
| gateway.annotations | object | `{}` | 应用于所有资源的注解 |
| gateway.autoscaling.enabled | bool | `false` | 启用网关的自动扩展功能 |
| gateway.autoscaling.maxReplicas | int | `5` | 最大副本数 |
| gateway.autoscaling.minReplicas | int | `1` | 最小副本数 |
| gateway.autoscaling.targetCPUUtilizationPercentage | int | `80` | CPU 使用率的目标百分比 |
| gateway.containerSecurityContext | string | `nil` | 网关容器的安全配置上下文 |
| gateway.env | object | `{}` | Pod 环境变量 |
| gateway.hostNetwork | bool | `false` | 是否使用主机网络 |
| gateway.httpPort | int | `80` | HTTP 服务端口 |
| gateway.httpsPort | int | `443` | HTTPS 服务端口 |
| gateway.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | 网关镜像的基础域名 |
| gateway.image | string | `"gateway"` | |
| gateway.kind | string | `"Deployment"` | 部署类型 |
| gateway.labels | object | `{}` | 应用于所有资源的标签 |
| gateway.metrics.enabled | bool | `false` | 启用网关度量收集 |
| gateway.metrics.honorLabels | bool | `false` | 是否合并现有标签 |
| gateway.metrics.interval | string | `""` | 度量间隔时间 |
| gateway.metrics.provider | string | `"monitoring.coreos.com"` | 定义监控提供者 |
| gateway.metrics.rawSpec | object | `{}` | 额外的度量规范 |
| gateway.metrics.relabelConfigs | list | `[]` | 重新标签配置 |
| gateway.metrics.relabelings | list | `[]` | 重新标签项 |
| gateway.metrics.scrapeTimeout | string | `""` | 抓取的超时时间 |
| gateway.name | string | `"higress-gateway"` | 网关名称 |
| gateway.networkGateway | string | `""` | 网络网关指定 |
| gateway.nodeSelector | object | `{}` | 节点选择器 |
| gateway.replicas | int | `2` | Higress Gateway pod 的数量 |
| gateway.resources.limits.cpu | string | `"2000m"` | 容器资源限制的 CPU |
| gateway.resources.limits.memory | string | `"2048Mi"` | 容器资源限制的内存 |
| gateway.resources.requests.cpu | string | `"2000m"` | 容器资源请求的 CPU |
| gateway.resources.requests.memory | string | `"2048Mi"` | 容器资源请求的内存 |
| gateway.revision | string | `""` | 网关所属版本声明 |
| gateway.rollingMaxSurge | string | `"100%"` | 最大激增数目百分比 |
| gateway.rollingMaxUnavailable | string | `"25%"` | 最大不可用比例 |
| gateway.readinessFailureThreshold | int | `30` | 成功尝试之前连续失败的最大探测次数 |
| gateway.readinessInitialDelaySeconds | int | `1` | 初次检测推迟多少秒后开始探测存活状态 |
| gateway.readinessPeriodSeconds | int | `2` | 存活探测间隔秒数 |
| gateway.readinessSuccessThreshold | int | `1` | 认为成功之前连续成功最小探测次数 |
| gateway.readinessTimeoutSeconds | int | `3` | 存活探测超时秒数 |
| gateway.securityContext | string | `nil` | 客户豆荚的安全上下文 |
| gateway.service.annotations | object | `{}` | 应用于服务账户的注释 |
| gateway.service.externalTrafficPolicy | string | `""` | 外部路由策略 |
| gateway.service.loadBalancerClass | string | `""` | 负载均衡器类别 |
| gateway.service.loadBalancerIP | string | `""` | 负载均衡器 IP 地址 |
| gateway.service.loadBalancerSourceRanges | list | `[]` | 允许访问负载均衡器的 CIDR 范围 |
| gateway.service.ports[0].name | string | `"http2"` | 服务定义的端口名称 |
| gateway.service.ports[0].port | int | `80` | 服务端口 |
| gateway.service.ports[0].protocol | string | `"TCP"` | 协议 |
| gateway.service.ports[0].targetPort | int | `80` | 靶向端口 |
| gateway.service.ports[1].name | string | `"https"` | 服务定义的端口名称 |
| gateway.service.ports[1].port | int | `443` | 服务端口 |
| gateway.service.ports[1].protocol | string | `"TCP"` | 协议 |
| gateway.service.ports[1].targetPort | int | `443` | 靶向端口 |
| gateway.service.type | string | `"LoadBalancer"` | 服务类型 |
| global.disableAlpnH2 | bool | `false` | 设置是否禁用 ALPN 中的 http/2 |
| ... | ... | ... | ... |
由于内容较多,其他参数可以参考完整表。

View File

@@ -242,15 +242,15 @@ require (
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/oauth2 v0.13.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/term v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/appengine v1.6.8 // indirect

View File

@@ -1789,8 +1789,9 @@ golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
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-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
@@ -1909,8 +1910,9 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
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=
@@ -1961,8 +1963,9 @@ golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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=
@@ -2069,8 +2072,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.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/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -2086,8 +2090,9 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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=
@@ -2108,8 +2113,9 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
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.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
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-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
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=

View File

@@ -704,9 +704,9 @@ func TestK8sObject_ResolveK8sConflict(t *testing.T) {
t.Run(tt.desc, func(t *testing.T) {
newObj := tt.o1.ResolveK8sConflict()
if !newObj.Equal(tt.o2) {
newObjjson, _ := newObj.JSON()
wantedObjjson, _ := tt.o2.JSON()
t.Errorf("Got: %s, want: %s", string(newObjjson), string(wantedObjjson))
newObjJson, _ := newObj.JSON()
wantedObjJson, _ := tt.o2.JSON()
t.Errorf("Got: %s, want: %s", string(newObjJson), string(wantedObjJson))
}
})
}

View File

@@ -65,7 +65,7 @@ func (o *K8sInstaller) Install() error {
return err1
}
fmt.Fprintf(o.writer, "\n✔ Wrote Profile in kubernetes configmap: \"%s\" \n", profileName)
fmt.Fprintf(o.writer, "\n Use bellow kubectl command to edit profile for upgrade. \n")
fmt.Fprintf(o.writer, "\n Use below kubectl command to edit profile for upgrade. \n")
fmt.Fprintf(o.writer, " ================================================================================== \n")
names := strings.Split(profileName, "/")
fmt.Fprintf(o.writer, " kubectl edit configmap %s -n %s \n", names[1], names[0])

View File

@@ -33,7 +33,8 @@ import (
"github.com/tidwall/gjson"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/wasm-go/pkg/wrapper"
logs "github.com/higress-group/wasm-go/pkg/log"
)
func main() {
@@ -72,13 +73,13 @@ type PluginConfig struct {
secondField string ` + "`required:\"true\"`" + `
}
func parseConfig(json gjson.Result, config *PluginConfig, log wrapper.Log) error {
func parseConfig(json gjson.Result, config *PluginConfig, log logs.Log) error {
config.firstField = json.Get("firstField").String()
config.secondField = json.Get("secondField").String()
return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log logs.Log) types.Action {
err := proxywasm.AddHttpRequestHeader(config.firstField, config.secondField)
if err != nil {
log.Critical("failed to set request header")
@@ -90,10 +91,10 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrap
module {{ .Name }}
go 1.19
go 1.24
require (
github.com/alibaba/higress/plugins/wasm-go main
github.com/higress-group/wasm-go main
github.com/higress-group/proxy-wasm-go-sdk main
github.com/tidwall/gjson v1.14.3
)

View File

@@ -93,6 +93,15 @@ func (p Protocol) IsUnsupported() bool {
return p == Unsupported
}
func (p Protocol) IsSupportedByProxy() bool {
switch p {
case HTTPS:
return true
default:
return false
}
}
func (p Protocol) String() string {
return string(p)
}

59
pkg/common/proxy.go Normal file
View File

@@ -0,0 +1,59 @@
// 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 common
import (
"strings"
)
type ProxyType string
const (
ProxyType_Unknown ProxyType = "Unknown"
ProxyType_HTTP ProxyType = "HTTP"
ProxyType_HTTPS ProxyType = "HTTPS"
ProxyType_SOCKS4 ProxyType = "SOCKS4"
ProxyType_SOCKS5 ProxyType = "SOCKS5"
)
func ParseProxyType(s string) ProxyType {
switch strings.ToLower(s) {
case "http":
return ProxyType_HTTP
case "https":
return ProxyType_HTTPS
case "socks4":
return ProxyType_SOCKS4
case "socks5":
return ProxyType_SOCKS5
}
return ProxyType_Unknown
}
func (p ProxyType) GetTransportProtocol() Protocol {
switch p {
case ProxyType_HTTP:
return HTTP
case ProxyType_HTTPS:
return HTTPS
case ProxyType_SOCKS4, ProxyType_SOCKS5:
return TCP
}
return Unsupported
}
func (p ProxyType) String() string {
return string(p)
}

View File

@@ -22,5 +22,6 @@ var (
GatewayName = env.RegisterStringVar("GATEWAY_NAME", "higress-gateway", "").Get()
// Revision is the value of the Istio control plane revision, e.g. "canary",
// and is the value used by the "istio.io/rev" label.
Revision = env.Register("REVISION", "", "").Get()
Revision = env.Register("REVISION", "", "").Get()
McpServerWasmImageUrl = env.RegisterStringVar("MCP_SERVER_WASM_IMAGE_URL", "oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/mcp-server/all-in-one:1.0.0", "").Get()
)

View File

@@ -63,12 +63,13 @@ import (
"github.com/alibaba/higress/pkg/ingress/kube/ingress"
"github.com/alibaba/higress/pkg/ingress/kube/ingressv1"
"github.com/alibaba/higress/pkg/ingress/kube/mcpbridge"
"github.com/alibaba/higress/pkg/ingress/kube/mcpserver"
"github.com/alibaba/higress/pkg/ingress/kube/secret"
"github.com/alibaba/higress/pkg/ingress/kube/util"
"github.com/alibaba/higress/pkg/ingress/kube/wasmplugin"
. "github.com/alibaba/higress/pkg/ingress/log"
"github.com/alibaba/higress/pkg/kube"
"github.com/alibaba/higress/registry/memory"
"github.com/alibaba/higress/registry"
"github.com/alibaba/higress/registry/reconcile"
)
@@ -152,11 +153,14 @@ type IngressConfig struct {
httpsConfigMgr *cert.ConfigMgr
commonOptions common.Options
// templateProcessor processes template variables in config
templateProcessor *TemplateProcessor
// secretConfigMgr manages secret dependencies
secretConfigMgr *SecretConfigMgr
mcpServerCache mcpserver.McpServerCache
}
// getSecretValue implements the getValue function for secret references
@@ -197,6 +201,7 @@ func NewIngressConfig(localKubeClient kube.Client, xdsUpdater istiomodel.XDSUpda
namespace: namespace,
wasmPlugins: make(map[string]*extensions.WasmPlugin),
http2rpcs: make(map[string]*higressv1.Http2Rpc),
commonOptions: options,
}
// Initialize secret config manager
@@ -222,6 +227,7 @@ func NewIngressConfig(localKubeClient kube.Client, xdsUpdater istiomodel.XDSUpda
higressConfigController := configmap.NewController(localKubeClient, clusterId, namespace)
config.configmapMgr = configmap.NewConfigmapMgr(xdsUpdater, namespace, higressConfigController, higressConfigController.Lister())
config.configmapMgr.RegisterMcpServerProvider(&config.mcpServerCache)
httpsConfigMgr, _ := cert.NewConfigMgr(namespace, localKubeClient.Kube())
config.httpsConfigMgr = httpsConfigMgr
@@ -334,10 +340,6 @@ func (m *IngressConfig) listFromIngressControllers(typ config.GroupVersionKind,
}
IngressLog.Infof("Append %d configmap EnvoyFilters", len(configmapEnvoyFilters))
}
if len(envoyFilters) == 0 {
IngressLog.Infof("resource type %s, configs number %d", typ, len(m.cachedEnvoyFilters))
return m.cachedEnvoyFilters
}
envoyFilters = append(envoyFilters, m.cachedEnvoyFilters...)
IngressLog.Infof("resource type %s, configs number %d", typ, len(envoyFilters))
return envoyFilters
@@ -419,6 +421,10 @@ func (m *IngressConfig) createWrapperConfigs(configs []config.Config) []common.W
m.watchedSecretSet = globalContext.WatchedSecrets
m.mutex.Unlock()
if m.mcpServerCache.SetMcpServers(globalContext.McpServers) {
m.notifyXDSFullUpdate(mcpserver.GvkMcpServer, "mcp-server-annotation-change", nil)
}
return wrapperConfigs
}
@@ -480,6 +486,22 @@ func (m *IngressConfig) convertVirtualService(configs []common.WrapperConfig) []
VirtualServices: map[string]*common.WrapperVirtualService{},
HTTPRoutes: map[string][]*common.WrapperHTTPRoute{},
Route2Ingress: map[string]*common.WrapperConfigWithRuleKey{},
ServiceWrappers: make(map[string]*common.ServiceWrapper),
ProxyWrappers: make(map[string]*common.ProxyWrapper),
}
if m.RegistryReconciler != nil {
for _, sew := range m.RegistryReconciler.GetAllServiceWrapper() {
hosts := sew.ServiceEntry.Hosts
if len(hosts) == 0 {
continue
}
for _, host := range hosts {
convertOptions.ServiceWrappers[host] = sew
}
}
for _, pw := range m.RegistryReconciler.GetAllProxyWrapper() {
convertOptions.ProxyWrappers[pw.ProxyName] = pw
}
}
// convert http route
@@ -588,6 +610,13 @@ func (m *IngressConfig) convertVirtualService(configs []common.WrapperConfig) []
Spec: vs,
})
}
// add vs from nacos3 for mcp server
if m.RegistryReconciler != nil {
allConfigsFromMcp := m.RegistryReconciler.GetAllConfigs(gvk.VirtualService)
for _, cfg := range allConfigsFromMcp {
out = append(out, *cfg)
}
}
// We generate some specific envoy filter here to avoid duplicated computation.
m.convertEnvoyFilter(&convertOptions)
@@ -599,6 +628,7 @@ func (m *IngressConfig) convertEnvoyFilter(convertOptions *common.ConvertOptions
mappings := map[string]*common.Rule{}
initHttp2RpcGlobalConfig := true
initMcpSseGlobalFilter := true
for _, routes := range convertOptions.HTTPRoutes {
for _, route := range routes {
if strings.HasSuffix(route.HTTPRoute.Name, "app-root") {
@@ -618,6 +648,19 @@ func (m *IngressConfig) convertEnvoyFilter(convertOptions *common.ConvertOptions
}
}
loadBalance := route.WrapperConfig.AnnotationsConfig.LoadBalance
if loadBalance != nil && loadBalance.McpSseStateful {
IngressLog.Infof("Found MCP SSE stateful session for route %s", route.HTTPRoute.Name)
envoyFilter, err := m.constructMcpSseStatefulSessionEnvoyFilter(route, m.namespace, initMcpSseGlobalFilter)
if err != nil {
IngressLog.Errorf("Construct MCP SSE stateful session EnvoyFilter error %v", err)
} else {
IngressLog.Infof("Append MCP SSE stateful session EnvoyFilter for route %s", route.HTTPRoute.Name)
envoyFilters = append(envoyFilters, *envoyFilter)
initMcpSseGlobalFilter = false
}
}
auth := route.WrapperConfig.AnnotationsConfig.Auth
if auth == nil {
continue
@@ -652,6 +695,12 @@ func (m *IngressConfig) convertEnvoyFilter(convertOptions *common.ConvertOptions
}
}
if proxyEnvoyFilters := constructProxyEnvoyFilters(convertOptions.ProxyWrappers, convertOptions.ServiceWrappers, m.namespace); len(proxyEnvoyFilters) != 0 {
for _, ef := range proxyEnvoyFilters {
envoyFilters = append(envoyFilters, *ef)
}
}
// TODO Support other envoy filters
IngressLog.Infof("Found %d number of envoyFilters", len(envoyFilters))
@@ -674,6 +723,13 @@ func (m *IngressConfig) convertWasmPlugin([]common.WrapperConfig) []config.Confi
Spec: wasmPlugin,
})
}
// add wasm plugin from nacos for mcp server
if m.RegistryReconciler != nil {
wasmFromMcp := m.RegistryReconciler.GetAllConfigs(gvk.WasmPlugin)
for _, cfg := range wasmFromMcp {
out = append(out, *cfg)
}
}
return out
}
@@ -684,6 +740,7 @@ func (m *IngressConfig) convertServiceEntry([]common.WrapperConfig) []config.Con
serviceEntries := m.RegistryReconciler.GetAllServiceWrapper()
IngressLog.Infof("Found mcp serviceEntries %v", serviceEntries)
out := make([]config.Config, 0, len(serviceEntries))
hostSets := sets.Set[string]{}
for _, se := range serviceEntries {
out = append(out, config.Config{
Meta: config.Meta{
@@ -698,6 +755,15 @@ func (m *IngressConfig) convertServiceEntry([]common.WrapperConfig) []config.Con
},
Spec: se.ServiceEntry,
})
hostSets.Insert(se.ServiceEntry.Hosts[0])
}
// add service entry by host from nacos3 for mcp server
seFromMcp := m.RegistryReconciler.GetAllConfigs(gvk.ServiceEntry)
for _, cfg := range seFromMcp {
se := cfg.Spec.(*networking.ServiceEntry)
if !hostSets.Contains(se.Hosts[0]) {
out = append(out, *cfg)
}
}
return out
}
@@ -768,19 +834,38 @@ func (m *IngressConfig) convertDestinationRule(configs []common.WrapperConfig) [
if !exist {
destinationRules[serviceName] = destinationRuleWrapper
} else if dr.DestinationRule.TrafficPolicy != nil {
portTrafficPolicy := destinationRuleWrapper.DestinationRule.TrafficPolicy.PortLevelSettings[0]
portUpdated := false
for _, policy := range dr.DestinationRule.TrafficPolicy.PortLevelSettings {
if policy.Port.Number == portTrafficPolicy.Port.Number {
policy.Tls = portTrafficPolicy.Tls
portUpdated = true
break
// if the service is referenced by an sse type mcp server, an source ip based consistent hashing policy needs to be configured
// consistent hashing policy will be generated by mcp server watcher, then if service do not have LoadBalancer settings, it will be merged
if destinationRuleWrapper.DestinationRule.TrafficPolicy != nil && destinationRuleWrapper.DestinationRule.TrafficPolicy.LoadBalancer != nil {
if dr.DestinationRule.TrafficPolicy.LoadBalancer == nil {
dr.DestinationRule.TrafficPolicy.LoadBalancer = destinationRuleWrapper.DestinationRule.TrafficPolicy.LoadBalancer
} else if dr.DestinationRule.TrafficPolicy.LoadBalancer.LbPolicy == nil {
dr.DestinationRule.TrafficPolicy.LoadBalancer.LbPolicy = destinationRuleWrapper.DestinationRule.TrafficPolicy.LoadBalancer.LbPolicy
}
}
if portUpdated {
continue
// if the service is referenced by an https type mcp server, an client side simple mode tls policy needs to be configured
// simple mode tls policy will be generated by mcp server watcher, then if service do not have tls settings, it will be merged
if dr.DestinationRule.TrafficPolicy.Tls == nil && destinationRuleWrapper.DestinationRule.TrafficPolicy != nil &&
destinationRuleWrapper.DestinationRule.TrafficPolicy.Tls != nil {
dr.DestinationRule.TrafficPolicy.Tls = destinationRuleWrapper.DestinationRule.TrafficPolicy.Tls
}
// Directly inherit or override the port policy (if it exists)
if len(destinationRuleWrapper.DestinationRule.TrafficPolicy.PortLevelSettings) > 0 {
portTrafficPolicy := destinationRuleWrapper.DestinationRule.TrafficPolicy.PortLevelSettings[0]
portUpdated := false
for _, policy := range dr.DestinationRule.TrafficPolicy.PortLevelSettings {
if policy.Port.Number == portTrafficPolicy.Port.Number {
policy.Tls = portTrafficPolicy.Tls
policy.LoadBalancer = portTrafficPolicy.LoadBalancer
portUpdated = true
break
}
}
if portUpdated {
continue
}
dr.DestinationRule.TrafficPolicy.PortLevelSettings = append(dr.DestinationRule.TrafficPolicy.PortLevelSettings, portTrafficPolicy)
}
dr.DestinationRule.TrafficPolicy.PortLevelSettings = append(dr.DestinationRule.TrafficPolicy.PortLevelSettings, portTrafficPolicy)
}
}
}
@@ -904,7 +989,7 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
result := &extensions.WasmPlugin{
Selector: &istiotype.WorkloadSelector{
MatchLabels: map[string]string{
"higress": m.namespace + "-higress-gateway",
m.commonOptions.GatewaySelectorKey: m.commonOptions.GatewaySelectorValue,
},
},
Url: obj.Url,
@@ -1060,7 +1145,7 @@ func (m *IngressConfig) AddOrUpdateWasmPlugin(clusterNamespacedName util.Cluster
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
}
for _, f := range m.wasmPluginHandlers {
IngressLog.Debug("WasmPlugin triggerd update")
IngressLog.Debug("WasmPlugin triggered update")
f(config.Config{Meta: metadata}, config.Config{Meta: metadata}, istiomodel.EventUpdate)
}
istioWasmPlugin, err := m.convertIstioWasmPlugin(&wasmPlugin.Spec)
@@ -1102,7 +1187,7 @@ func (m *IngressConfig) DeleteWasmPlugin(clusterNamespacedName util.ClusterNames
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
}
for _, f := range m.wasmPluginHandlers {
IngressLog.Debug("WasmPlugin triggerd update")
IngressLog.Debug("WasmPlugin triggered update")
f(config.Config{Meta: metadata}, config.Config{Meta: metadata}, istiomodel.EventDelete)
}
}
@@ -1135,15 +1220,50 @@ func (m *IngressConfig) AddOrUpdateMcpBridge(clusterNamespacedName util.ClusterN
// Set this label so that we do not compare configs and just push.
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
}
vsMetadata := config.Meta{
Name: "mcpbridge-virtualservice",
Namespace: m.namespace,
GroupVersionKind: gvk.VirtualService,
// Set this label so that we do not compare configs and just push.
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
}
wasmMetadata := config.Meta{
Name: "mcpbridge-wasmplugin",
Namespace: m.namespace,
GroupVersionKind: gvk.WasmPlugin,
// Set this label so that we do not compare configs and just push.
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
}
efMetadata := config.Meta{
Name: "mcpbridge-envoyfilter",
Namespace: m.namespace,
GroupVersionKind: gvk.EnvoyFilter,
// Set this label so that we do not compare configs and just push.
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
}
for _, f := range m.serviceEntryHandlers {
IngressLog.Debug("McpBridge triggerd serviceEntry update")
IngressLog.Debug("McpBridge triggered serviceEntry update")
f(config.Config{Meta: seMetadata}, config.Config{Meta: seMetadata}, istiomodel.EventUpdate)
}
for _, f := range m.destinationRuleHandlers {
IngressLog.Debug("McpBridge triggerd destinationRule update")
IngressLog.Debug("McpBridge triggered destinationRule update")
f(config.Config{Meta: drMetadata}, config.Config{Meta: drMetadata}, istiomodel.EventUpdate)
}
}, m.localKubeClient, m.namespace)
for _, f := range m.virtualServiceHandlers {
IngressLog.Debug("McpBridge triggered virtualservice update")
f(config.Config{Meta: vsMetadata}, config.Config{Meta: vsMetadata}, istiomodel.EventUpdate)
}
for _, f := range m.wasmPluginHandlers {
IngressLog.Debug("McpBridge triggered wasmplugin update")
f(config.Config{Meta: wasmMetadata}, config.Config{Meta: wasmMetadata}, istiomodel.EventUpdate)
}
for _, f := range m.envoyFilterHandlers {
IngressLog.Debug("McpBridge triggered envoyfilter update")
f(config.Config{Meta: efMetadata}, config.Config{Meta: efMetadata}, istiomodel.EventUpdate)
}
}, m.localKubeClient, m.namespace, m.clusterId.String())
m.configmapMgr.RegisterMcpServerProvider(m.RegistryReconciler)
}
reconciler := m.RegistryReconciler
err = reconciler.Reconcile(mcpbridge)
@@ -1207,7 +1327,7 @@ func (m *IngressConfig) DeleteHttp2Rpc(clusterNamespacedName util.ClusterNamespa
}
m.mutex.Unlock()
if hit {
IngressLog.Infof("Http2Rpc triggerd deleted event executed %s", clusterNamespacedName.Name)
IngressLog.Infof("Http2Rpc triggered deleted event executed %s", clusterNamespacedName.Name)
push := func(gvk config.GroupVersionKind) {
m.XDSUpdater.ConfigUpdate(&istiomodel.PushRequest{
Full: true,
@@ -1405,7 +1525,7 @@ func (m *IngressConfig) constructHttp2RpcEnvoyFilter(http2rpcConfig *annotations
return &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.EnvoyFilter,
Name: common.CreateConvertedName(constants.IstioIngressGatewayName, http2rpcConfig.Name),
Name: common.CreateConvertedName(constants.IstioIngressGatewayName, "http2rpc", http2rpcConfig.Name, "route", common.ConvertToDNSLabelValid(httpRoute.Name)),
Namespace: namespace,
},
Spec: &networking.EnvoyFilter{
@@ -1587,28 +1707,150 @@ func constructBasicAuthEnvoyFilter(rules *common.BasicAuthRules, namespace strin
}, nil
}
func QueryByName(serviceEntries []*memory.ServiceWrapper, serviceName string) (*memory.ServiceWrapper, error) {
IngressLog.Infof("Found http2rpc serviceEntries %s", serviceEntries)
for _, se := range serviceEntries {
if se.ServiceName == serviceName {
return se, nil
func constructProxyEnvoyFilters(proxyWrappers map[string]*common.ProxyWrapper, serviceWrappers map[string]*common.ServiceWrapper, namespace string) []*config.Config {
var envoyFilters []*config.Config
for _, proxyWrapper := range proxyWrappers {
envoyFilters = append(envoyFilters, &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.EnvoyFilter,
Name: common.CreateConvertedName(constants.IstioIngressGatewayName, "proxy", proxyWrapper.ProxyName),
Namespace: namespace,
},
Spec: proxyWrapper.EnvoyFilter,
})
}
// Create a cluster for each service that uses a proxy.
var serviceProxyPatches []*networking.EnvoyFilter_EnvoyConfigObjectPatch
for _, serviceWrapper := range serviceWrappers {
proxyConfig := serviceWrapper.ProxyConfig
if proxyConfig == nil || proxyConfig.ProxyName == "" {
continue
}
IngressLog.Debugf("Found service %s using proxy %s", serviceWrapper.ServiceName, proxyConfig.ProxyName)
if err := validateServiceWrapperForProxy(serviceWrapper); err != nil {
IngressLog.Warnf("Service wrapper validation failed for proxy: %v", err)
continue
}
proxyWrapper := proxyWrappers[proxyConfig.ProxyName]
if proxyWrapper == nil {
IngressLog.Warnf("Service %s has proxy config %s, but no corresponding proxy wrapper found", serviceWrapper.ServiceName, proxyConfig.ProxyName)
continue
}
if !proxyConfig.UpstreamProtocol.IsSupportedByProxy() {
IngressLog.Warnf("Proxy %s does not support upstream protocol %s, skipping EnvoyFilter construction for service %s")
continue
}
if proxyWrapper.EnvoyFilter == nil {
IngressLog.Warnf("Proxy %s has no EnvoyFilter generated, meaning not ready for use.", proxyConfig.ProxyName)
continue
}
se := serviceWrapper.ServiceEntry
if se == nil || len(se.Hosts) == 0 || len(se.Ports) == 0 {
continue
}
for _, host := range se.Hosts {
IngressLog.Debugf("Constructing EnvoyFilter for service %s using proxy %s", host, proxyConfig.ProxyName)
for _, port := range se.Ports {
if port == nil || port.Number <= 0 {
continue
}
clusterName := fmt.Sprintf("outbound|%d||%s", port.Number, host)
// We need to delete the original cluster and add a new one pointing to the local proxy listener.
serviceProxyPatches = append(serviceProxyPatches, &networking.EnvoyFilter_EnvoyConfigObjectPatch{
ApplyTo: networking.EnvoyFilter_CLUSTER,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
Context: networking.EnvoyFilter_GATEWAY,
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Cluster{
Cluster: &networking.EnvoyFilter_ClusterMatch{
Name: clusterName,
},
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_REMOVE,
},
})
patchObj := map[string]interface{}{
"name": clusterName,
"type": "STATIC",
"connect_timeout": "10s",
"load_assignment": map[string]interface{}{
"cluster_name": clusterName,
"endpoints": []map[string]interface{}{
{
"lb_endpoints": []map[string]interface{}{
{
"endpoint": map[string]interface{}{
"address": map[string]interface{}{
"socket_address": map[string]interface{}{
"address": "127.0.0.1",
"port_value": proxyWrapper.ListenerPort,
},
},
},
},
},
},
},
},
}
if proxyConfig.UpstreamProtocol.IsHTTPS() {
tlsTypedConfig := map[string]interface{}{
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
}
if proxyConfig.UpstreamSni != "" {
tlsTypedConfig["sni"] = proxyConfig.UpstreamSni
}
patchObj["transport_socket"] = map[string]interface{}{
"name": "envoy.transport_sockets.tls",
"typed_config": tlsTypedConfig,
}
}
patchJson, _ := json.Marshal(patchObj)
serviceProxyPatches = append(serviceProxyPatches, &networking.EnvoyFilter_EnvoyConfigObjectPatch{
ApplyTo: networking.EnvoyFilter_CLUSTER,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
Context: networking.EnvoyFilter_GATEWAY,
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_ADD,
Value: util.BuildPatchStruct(string(patchJson)),
},
})
}
}
}
return nil, fmt.Errorf("can't find ServiceEntry by serviceName:%v", serviceName)
if len(serviceProxyPatches) != 0 {
envoyFilters = append(envoyFilters, &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.EnvoyFilter,
Name: common.CreateConvertedName(constants.IstioIngressGatewayName, "service-proxy"),
Namespace: namespace,
},
Spec: &networking.EnvoyFilter{
ConfigPatches: serviceProxyPatches,
},
})
}
return envoyFilters
}
func QueryRpcServiceVersion(serviceEntry *memory.ServiceWrapper, serviceName string) (string, error) {
IngressLog.Infof("Found http2rpc serviceEntry %s", serviceEntry)
IngressLog.Infof("Found http2rpc ServiceEntry %s", serviceEntry.ServiceEntry)
IngressLog.Infof("Found http2rpc WorkloadSelector %s", serviceEntry.ServiceEntry.WorkloadSelector)
IngressLog.Infof("Found http2rpc Labels %s", serviceEntry.ServiceEntry.WorkloadSelector.Labels)
labels := (*serviceEntry).ServiceEntry.WorkloadSelector.Labels
for key, value := range labels {
if key == "version" {
return value, nil
}
func validateServiceWrapperForProxy(serviceWrapper *common.ServiceWrapper) error {
registryType := registry.ServiceRegistryType(serviceWrapper.RegistryType)
switch registryType {
case registry.DNS:
break
default:
return fmt.Errorf("service %s has proxy config %s, but registry type %s is not supported for proxying", serviceWrapper.ServiceName, serviceWrapper.ProxyConfig.ProxyName, registryType)
}
return "", fmt.Errorf("can't get RpcServiceVersion for serviceName:%v", serviceName)
if len(serviceWrapper.ServiceEntry.Endpoints) > 1 {
return fmt.Errorf("service %s has multiple endpoints, which is not supported for proxying with EnvoyFilter. Skipping EnvoyFilter construction", serviceWrapper.ServiceName)
}
return nil
}
func (m *IngressConfig) Run(stop <-chan struct{}) {
@@ -1711,3 +1953,112 @@ func (m *IngressConfig) Patch(config.Config, config.PatchFunc) (string, error) {
func (m *IngressConfig) Delete(config.GroupVersionKind, string, string, *string) error {
return common.ErrUnsupportedOp
}
func (m *IngressConfig) constructMcpSseStatefulSessionEnvoyFilter(route *common.WrapperHTTPRoute, namespace string, initGlobalFilter bool) (*config.Config, error) {
httpRoute := route.HTTPRoute
var configPatches []*networking.EnvoyFilter_EnvoyConfigObjectPatch
// Add global HTTP filter if this is the first route using MCP SSE stateful session
if initGlobalFilter {
configPatches = append(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.router",
},
},
},
},
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_INSERT_BEFORE,
Value: buildPatchStruct(`{
"name": "envoy.filters.http.mcp_sse_stateful_session",
"typed_config": {
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
"type_url": "type.googleapis.com/envoy.extensions.filters.http.mcp_sse_stateful_session.v3alpha.McpSseStatefulSession"
}
}`),
},
})
}
// Add route-specific configuration
configPatches = append(configPatches, &networking.EnvoyFilter_EnvoyConfigObjectPatch{
ApplyTo: networking.EnvoyFilter_HTTP_ROUTE,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
Context: networking.EnvoyFilter_GATEWAY,
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_RouteConfiguration{
RouteConfiguration: &networking.EnvoyFilter_RouteConfigurationMatch{
Vhost: &networking.EnvoyFilter_RouteConfigurationMatch_VirtualHostMatch{
Route: &networking.EnvoyFilter_RouteConfigurationMatch_RouteMatch{
Name: httpRoute.Name,
},
},
},
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_MERGE,
Value: buildPatchStruct(`{
"typed_per_filter_config": {
"envoy.filters.http.mcp_sse_stateful_session": {
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
"type_url": "type.googleapis.com/envoy.extensions.filters.http.mcp_sse_stateful_session.v3alpha.McpSseStatefulSessionPerRoute",
"value": {
"mcp_sse_stateful_session": {
"session_state": {
"name": "envoy.http.mcp_sse_stateful_session.envelope",
"typed_config": {
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
"type_url": "type.googleapis.com/envoy.extensions.http.mcp_sse_stateful_session.envelope.v3alpha.EnvelopeSessionState",
"value": {
"param_name": "sessionId",
"chunk_end_patterns": ["\r\n\r\n", "\n\n", "\r\r"]
}
}
},
"strict": true
}
}
}
}
}`),
},
})
return &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.EnvoyFilter,
Name: common.CreateConvertedName(constants.IstioIngressGatewayName, "mcp-lb-route", common.ConvertToDNSLabelValid(httpRoute.Name)),
Namespace: namespace,
},
Spec: &networking.EnvoyFilter{
ConfigPatches: configPatches,
},
}, nil
}
func (m *IngressConfig) notifyXDSFullUpdate(gvk config.GroupVersionKind, reason istiomodel.TriggerReason, updatedConfigName *util.ClusterNamespacedName) {
var configsUpdated map[istiomodel.ConfigKey]struct{}
if updatedConfigName != nil {
configsUpdated = map[istiomodel.ConfigKey]struct{}{{
Kind: kind.MustFromGVK(gvk),
Name: updatedConfigName.Name,
Namespace: updatedConfigName.Namespace,
}: {}}
}
m.XDSUpdater.ConfigUpdate(&istiomodel.PushRequest{
Full: true,
ConfigsUpdated: configsUpdated,
Reason: istiomodel.NewReasonStats(reason),
})
}

View File

@@ -21,6 +21,8 @@ import (
"istio.io/istio/pkg/cluster"
"istio.io/istio/pkg/util/sets"
listersv1 "k8s.io/client-go/listers/core/v1"
"github.com/alibaba/higress/pkg/ingress/kube/mcpserver"
)
type GlobalContext struct {
@@ -30,6 +32,8 @@ type GlobalContext struct {
ClusterSecretLister map[cluster.ID]listersv1.SecretLister
ClusterServiceList map[cluster.ID]listersv1.ServiceLister
McpServers []*mcpserver.McpServer
}
type Meta struct {
@@ -169,6 +173,7 @@ func NewAnnotationHandlerManager() AnnotationHandler {
match{},
headerControl{},
http2rpc{},
mcpServer{},
},
gatewayHandlers: []GatewayHandler{
downstreamTLS{},

View File

@@ -66,9 +66,10 @@ type consistentHashByCookie struct {
}
type LoadBalanceConfig struct {
simple networking.LoadBalancerSettings_SimpleLB
other *consistentHashByOther
cookie *consistentHashByCookie
simple networking.LoadBalancerSettings_SimpleLB
other *consistentHashByOther
cookie *consistentHashByCookie
McpSseStateful bool
}
type loadBalance struct{}
@@ -129,7 +130,11 @@ func (l loadBalance) Parse(annotations Annotations, config *Ingress, _ *GlobalCo
} else {
if lb, err := annotations.ParseStringASAP(loadBalanceAnnotation); err == nil {
lb = strings.ToUpper(lb)
loadBalanceConfig.simple = networking.LoadBalancerSettings_SimpleLB(networking.LoadBalancerSettings_SimpleLB_value[lb])
if lb == "MCP-SSE" {
loadBalanceConfig.McpSseStateful = true
} else {
loadBalanceConfig.simple = networking.LoadBalancerSettings_SimpleLB(networking.LoadBalancerSettings_SimpleLB_value[lb])
}
}
}

View File

@@ -0,0 +1,94 @@
// Copyright (c) 2023 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 (
"strings"
"github.com/alibaba/higress/pkg/ingress/kube/mcpserver"
"github.com/alibaba/higress/pkg/ingress/log"
)
const (
enableMcpServer = "mcp-server"
mcpServerMatchRuleDomains = "mcp-server-match-rule-domains"
mcpServerMatchRuleType = "mcp-server-match-rule-type"
mcpServerMatchRuleValue = "mcp-server-match-rule-value"
mcpServerUpstreamType = "mcp-server-upstream-type"
mcpServerEnablePathRewrite = "mcp-server-enable-path-rewrite"
mcpServerPathRewritePrefix = "mcp-server-path-rewrite-prefix"
)
// help to conform mcpServer implements method of Parse
var (
_ Parser = &mcpServer{}
)
type mcpServer struct{}
func (a mcpServer) Parse(annotations Annotations, config *Ingress, globalContext *GlobalContext) error {
if globalContext == nil {
return nil
}
ingressKey := config.Namespace + "/" + config.Name
enabled, _ := annotations.ParseBoolASAP(enableMcpServer)
if !enabled {
return nil
}
var matchRuleDomains []string
rawMatchRuleDomains, _ := annotations.ParseStringASAP(mcpServerMatchRuleDomains)
if rawMatchRuleDomains == "" || rawMatchRuleDomains == "*" {
// Match all domains. Leave an empty slice.
} else if strings.Contains(rawMatchRuleDomains, ",") {
matchRuleDomains = strings.Split(rawMatchRuleDomains, ",")
} else {
matchRuleDomains = []string{rawMatchRuleDomains}
}
matchRuleType, _ := annotations.ParseStringASAP(mcpServerMatchRuleType)
if matchRuleType == "" {
log.IngressLog.Errorf("ingress %s: mcp-server-match-rule-path-type is empty", ingressKey)
return nil
} else if !mcpserver.ValidPathMatchTypes[matchRuleType] {
log.IngressLog.Errorf("ingress %s: mcp-server-match-rule-path-type %s is not supported", ingressKey, matchRuleType)
return nil
}
matchRuleValue, _ := annotations.ParseStringASAP(mcpServerMatchRuleValue)
upstreamType, _ := annotations.ParseStringASAP(mcpServerUpstreamType)
if upstreamType != "" && !mcpserver.ValidUpstreamTypes[upstreamType] {
log.IngressLog.Errorf("mcp-server-upstream-type %s is not supported", upstreamType)
return nil
}
enablePathRewrite, _ := annotations.ParseBoolASAP(mcpServerEnablePathRewrite)
pathRewritePrefix, _ := annotations.ParseStringASAP(mcpServerPathRewritePrefix)
globalContext.McpServers = append(globalContext.McpServers, &mcpserver.McpServer{
Name: ingressKey,
Domains: matchRuleDomains,
PathMatchType: matchRuleType,
PathMatchValue: matchRuleValue,
UpstreamType: upstreamType,
EnablePathRewrite: enablePathRewrite,
PathRewritePrefix: pathRewritePrefix,
})
return nil
}

View File

@@ -0,0 +1,257 @@
// Copyright (c) 2025 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 (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/alibaba/higress/pkg/ingress/kube/mcpserver"
)
func TestMCPServer_Parse(t *testing.T) {
parser := mcpServer{}
testCases := []struct {
skip bool
input Annotations
expect *mcpserver.McpServer
}{
{
// No annotation
input: Annotations{},
expect: nil,
},
{
// Not enabled
input: Annotations{
buildHigressAnnotationKey(enableMcpServer): "false",
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
buildHigressAnnotationKey(mcpServerMatchRuleType): "prefix",
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "",
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "",
},
expect: nil,
},
{
// Enabled but no match rule type
input: Annotations{
buildHigressAnnotationKey(enableMcpServer): "true",
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "",
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "",
},
expect: nil,
},
{
// Enabled but empty match rule type
input: Annotations{
buildHigressAnnotationKey(enableMcpServer): "true",
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
buildHigressAnnotationKey(mcpServerMatchRuleType): "",
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "",
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "",
},
expect: nil,
},
{
// Enabled but bad match rule type
input: Annotations{
buildHigressAnnotationKey(enableMcpServer): "true",
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
buildHigressAnnotationKey(mcpServerMatchRuleType): "bad-type",
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "",
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "",
},
expect: nil,
},
{
// Enabled but bad upstream type
input: Annotations{
buildHigressAnnotationKey(enableMcpServer): "true",
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
buildHigressAnnotationKey(mcpServerMatchRuleType): "prefix",
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
buildHigressAnnotationKey(mcpServerUpstreamType): "bad-type",
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "",
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "",
},
expect: nil,
},
{
// Enabled and rewrite not enabled
input: Annotations{
buildHigressAnnotationKey(enableMcpServer): "true",
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
buildHigressAnnotationKey(mcpServerMatchRuleType): "prefix",
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "false",
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "/",
},
expect: &mcpserver.McpServer{
Name: "default/route",
Domains: []string{"www.foo.com"},
PathMatchType: "prefix",
PathMatchValue: "/mcp",
UpstreamType: "rest",
EnablePathRewrite: false,
PathRewritePrefix: "/",
},
},
{
// Enabled and rewrite not enabled and empty domain
input: Annotations{
buildHigressAnnotationKey(enableMcpServer): "true",
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "",
buildHigressAnnotationKey(mcpServerMatchRuleType): "prefix",
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "false",
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "/",
},
expect: &mcpserver.McpServer{
Name: "default/route",
Domains: nil,
PathMatchType: "prefix",
PathMatchValue: "/mcp",
UpstreamType: "rest",
EnablePathRewrite: false,
PathRewritePrefix: "/",
},
},
{
// Enabled and rewrite not enabled and wildcard domain
input: Annotations{
buildHigressAnnotationKey(enableMcpServer): "true",
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "*",
buildHigressAnnotationKey(mcpServerMatchRuleType): "prefix",
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "false",
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "/",
},
expect: &mcpserver.McpServer{
Name: "default/route",
Domains: nil,
PathMatchType: "prefix",
PathMatchValue: "/mcp",
UpstreamType: "rest",
EnablePathRewrite: false,
PathRewritePrefix: "/",
},
},
{
// Enabled and rewrite enabled with root
input: Annotations{
buildHigressAnnotationKey(enableMcpServer): "true",
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
buildHigressAnnotationKey(mcpServerMatchRuleType): "prefix",
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "true",
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "/",
},
expect: &mcpserver.McpServer{
Name: "default/route",
Domains: []string{"www.foo.com"},
PathMatchType: "prefix",
PathMatchValue: "/mcp",
UpstreamType: "rest",
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
},
{
// Enabled and rewrite enabled with root
input: Annotations{
buildHigressAnnotationKey(enableMcpServer): "true",
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
buildHigressAnnotationKey(mcpServerMatchRuleType): "prefix",
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "true",
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "/mcp-api",
},
expect: &mcpserver.McpServer{
Name: "default/route",
Domains: []string{"www.foo.com"},
PathMatchType: "prefix",
PathMatchValue: "/mcp",
UpstreamType: "rest",
EnablePathRewrite: true,
PathRewritePrefix: "/mcp-api",
},
},
{
// Enabled and multiple domains
input: Annotations{
buildHigressAnnotationKey(enableMcpServer): "true",
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com,www.bar.com",
buildHigressAnnotationKey(mcpServerMatchRuleType): "exact",
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
buildHigressAnnotationKey(mcpServerUpstreamType): "sse",
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "true",
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "/",
},
expect: &mcpserver.McpServer{
Name: "default/route",
Domains: []string{"www.foo.com", "www.bar.com"},
PathMatchType: "exact",
PathMatchValue: "/mcp",
UpstreamType: "sse",
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
},
}
for _, tt := range testCases {
if tt.skip {
return
}
t.Run("", func(t *testing.T) {
config := &Ingress{Meta: Meta{
Namespace: "default",
Name: "route",
}}
globalContext := &GlobalContext{}
_ = parser.Parse(tt.input, config, globalContext)
if tt.expect == nil {
if len(globalContext.McpServers) != 0 {
t.Fatalf("globalContext.McpServers is not empty: %v", globalContext.McpServers)
}
return
}
if len(globalContext.McpServers) != 1 {
t.Fatalf("globalContext.McpServers length is not 1: %v", globalContext.McpServers)
}
if diff := cmp.Diff(tt.expect, globalContext.McpServers[0]); diff != "" {
t.Fatalf("TestMCPServer_Parse() mismatch (-want +got):\n%s", diff)
}
})
}
}

View File

@@ -22,8 +22,10 @@ import (
)
const (
mirrorTargetService = "mirror-target-service"
mirrorPercentage = "mirror-percentage"
mirrorTargetService = "mirror-target-service"
mirrorPercentage = "mirror-percentage"
mirrorTargetFQDN = "mirror-target-fqdn"
mirrorTargetFQDNPort = "mirror-target-fqdn-port"
)
var (
@@ -34,6 +36,8 @@ var (
type MirrorConfig struct {
util.ServiceInfo
Percentage *wrappers.DoubleValue
FQDN string
FPort uint32 // Port for FQDN
}
type mirror struct{}
@@ -43,6 +47,24 @@ func (m mirror) Parse(annotations Annotations, config *Ingress, globalContext *G
return nil
}
// if FQDN is set, then parse FQDN
if fqdn, err := annotations.ParseStringASAP(mirrorTargetFQDN); err == nil {
// default is 80
var port uint32
port = 80
if p, err := annotations.ParseInt32ASAP(mirrorTargetFQDNPort); err == nil {
port = uint32(p)
}
config.Mirror = &MirrorConfig{
Percentage: parsePercentage(annotations),
FQDN: fqdn,
FPort: port,
}
return nil
}
target, err := annotations.ParseStringASAP(mirrorTargetService)
if err != nil {
IngressLog.Errorf("Get mirror target service fail, err: %v", err)
@@ -78,7 +100,16 @@ func (m mirror) Parse(annotations Annotations, config *Ingress, globalContext *G
serviceInfo.Port = uint32(service.Spec.Ports[0].Port)
}
config.Mirror = &MirrorConfig{
ServiceInfo: serviceInfo,
Percentage: parsePercentage(annotations),
}
return nil
}
func parsePercentage(annotations Annotations) *wrappers.DoubleValue {
var percentage *wrappers.DoubleValue
if value, err := annotations.ParseIntASAP(mirrorPercentage); err == nil {
if value < 100 {
percentage = &wrappers.DoubleValue{
@@ -86,12 +117,7 @@ func (m mirror) Parse(annotations Annotations, config *Ingress, globalContext *G
}
}
}
config.Mirror = &MirrorConfig{
ServiceInfo: serviceInfo,
Percentage: percentage,
}
return nil
return percentage
}
func (m mirror) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
@@ -99,10 +125,21 @@ func (m mirror) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
return
}
var mirrorHost string
var mirrorPort uint32
if config.Mirror.FQDN != "" {
mirrorHost = config.Mirror.FQDN
mirrorPort = config.Mirror.FPort
} else {
mirrorHost = util.CreateServiceFQDN(config.Mirror.Namespace, config.Mirror.Name)
mirrorPort = config.Mirror.Port
}
route.Mirror = &networking.Destination{
Host: util.CreateServiceFQDN(config.Mirror.Namespace, config.Mirror.Name),
Host: mirrorHost,
Port: &networking.PortSelector{
Number: config.Mirror.Port,
Number: mirrorPort,
},
}
@@ -114,5 +151,5 @@ func (m mirror) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
}
func needMirror(annotations Annotations) bool {
return annotations.HasASAP(mirrorTargetService)
return annotations.HasASAP(mirrorTargetService) || annotations.HasASAP(mirrorTargetFQDN)
}

View File

@@ -15,12 +15,13 @@
package annotations
import (
"reflect"
"testing"
"github.com/alibaba/higress/pkg/ingress/kube/util"
"github.com/golang/protobuf/proto"
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pilot/pkg/model"
"reflect"
"testing"
)
func TestParseMirror(t *testing.T) {
@@ -29,6 +30,28 @@ func TestParseMirror(t *testing.T) {
expect *MirrorConfig
}{
{},
{
input: []map[string]string{
{buildHigressAnnotationKey(mirrorTargetFQDN): "www.example.com"},
{buildNginxAnnotationKey(mirrorTargetFQDN): "www.example.com"},
},
expect: &MirrorConfig{
ServiceInfo: util.ServiceInfo{},
FQDN: "www.example.com",
FPort: 80,
},
},
{
input: []map[string]string{
{buildHigressAnnotationKey(mirrorTargetFQDN): "192.168.252.112", buildHigressAnnotationKey(mirrorTargetFQDNPort): "8080"},
{buildNginxAnnotationKey(mirrorTargetFQDN): "192.168.252.112", buildNginxAnnotationKey(mirrorTargetFQDNPort): "8080"},
},
expect: &MirrorConfig{
ServiceInfo: util.ServiceInfo{},
FQDN: "192.168.252.112",
FPort: 8080,
},
},
{
input: []map[string]string{
{buildHigressAnnotationKey(mirrorTargetService): "test/app"},
@@ -149,6 +172,42 @@ func TestMirror_ApplyRoute(t *testing.T) {
},
},
},
{
config: &Ingress{
Mirror: &MirrorConfig{
ServiceInfo: util.ServiceInfo{},
FQDN: "www.example.com",
FPort: 80,
},
},
input: &networking.HTTPRoute{},
expect: &networking.HTTPRoute{
Mirror: &networking.Destination{
Host: "www.example.com",
Port: &networking.PortSelector{
Number: 80,
},
},
},
},
{
config: &Ingress{
Mirror: &MirrorConfig{
ServiceInfo: util.ServiceInfo{},
FQDN: "192.168.252.112",
FPort: 8080,
},
},
input: &networking.HTTPRoute{},
expect: &networking.HTTPRoute{
Mirror: &networking.Destination{
Host: "192.168.252.112",
Port: &networking.PortSelector{
Number: 8080,
},
},
},
},
}
mirror := mirror{}

View File

@@ -16,9 +16,8 @@ package common
import (
"strings"
"time"
"github.com/alibaba/higress/pkg/cert"
"github.com/alibaba/higress/pkg/ingress/kube/annotations"
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pilot/pkg/model"
"istio.io/istio/pkg/cluster"
@@ -26,6 +25,10 @@ import (
gatewaytool "istio.io/istio/pkg/config/gateway"
listerv1 "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
"github.com/alibaba/higress/pkg/cert"
"github.com/alibaba/higress/pkg/common"
"github.com/alibaba/higress/pkg/ingress/kube/annotations"
)
type ServiceKey struct {
@@ -120,6 +123,68 @@ type WrapperDestinationRule struct {
ServiceKey ServiceKey
}
type ServiceProxyConfig struct {
ProxyName string
UpstreamProtocol common.Protocol
UpstreamSni string
}
type ServiceWrapper struct {
ServiceName string
ServiceEntry *networking.ServiceEntry
DestinationRuleWrapper *WrapperDestinationRule
Suffix string
RegistryType string
RegistryName string
ProxyConfig *ServiceProxyConfig
createTime time.Time
}
func (sew *ServiceWrapper) DeepCopy() *ServiceWrapper {
res := &ServiceWrapper{}
*res = *sew
res.ServiceEntry = sew.ServiceEntry.DeepCopy()
if sew.DestinationRuleWrapper != nil {
res.DestinationRuleWrapper = sew.DestinationRuleWrapper
res.DestinationRuleWrapper.DestinationRule = sew.DestinationRuleWrapper.DestinationRule.DeepCopy()
}
return res
}
func (sew *ServiceWrapper) SetCreateTime(createTime time.Time) {
sew.createTime = createTime
}
func (sew *ServiceWrapper) GetCreateTime() time.Time {
return sew.createTime
}
type ProxyWrapper struct {
ProxyName string
ListenerPort uint32
EnvoyFilter *networking.EnvoyFilter
createTime time.Time
}
func (pw *ProxyWrapper) DeepCopy() *ProxyWrapper {
res := &ProxyWrapper{}
*res = *pw
if pw.EnvoyFilter != nil {
res.EnvoyFilter = pw.EnvoyFilter.DeepCopy()
}
return res
}
func (pw *ProxyWrapper) SetCreateTime(createTime time.Time) {
pw.createTime = createTime
}
func (pw *ProxyWrapper) GetCreateTime() time.Time {
return pw.createTime
}
type IngressController interface {
// RegisterEventHandler adds a handler to receive config update events for a
// configuration type

View File

@@ -169,6 +169,10 @@ type ConvertOptions struct {
Service2TrafficPolicy map[ServiceKey]*WrapperTrafficPolicy
ServiceWrappers map[string]*ServiceWrapper
ProxyWrappers map[string]*ProxyWrapper
HasDefaultBackend bool
}

View File

@@ -0,0 +1,97 @@
// 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 common
import (
"testing"
"github.com/stretchr/testify/assert"
"istio.io/istio/pilot/pkg/model"
"istio.io/istio/pkg/config"
)
func TestIngressDomainCache(t *testing.T) {
cache := NewIngressDomainCache()
assert.NotNil(t, cache)
assert.NotNil(t, cache.Valid)
assert.Empty(t, cache.Invalid)
cache.Valid["example.com"] = &IngressDomainBuilder{
Host: "example.com",
Protocol: HTTP,
ClusterId: "cluster-1",
Ingress: &config.Config{
Meta: config.Meta{
Name: "test-ingress",
Namespace: "default",
},
},
}
cache.Invalid = append(cache.Invalid, model.IngressDomain{
Host: "invalid.com",
Error: "invalid domain",
})
result := cache.Extract()
assert.Equal(t, 1, len(result.Valid))
assert.Equal(t, "example.com", result.Valid[0].Host)
assert.Equal(t, string(HTTP), result.Valid[0].Protocol)
assert.Equal(t, 1, len(result.Invalid))
assert.Equal(t, "invalid.com", result.Invalid[0].Host)
}
func TestIngressDomainBuilder(t *testing.T) {
builder := &IngressDomainBuilder{
Host: "example.com",
Protocol: HTTP,
ClusterId: "cluster-1",
Ingress: &config.Config{
Meta: config.Meta{
Name: "test-ingress",
Namespace: "default",
},
},
}
domain := builder.Build()
assert.Equal(t, "example.com", domain.Host)
assert.Equal(t, string(HTTP), domain.Protocol)
builder.Event = MissingSecret
eventDomain := builder.Build()
assert.Contains(t, eventDomain.Error, "misses secret")
builder.Event = DuplicatedTls
builder.PreIngress = &config.Config{
Meta: config.Meta{
Name: "pre-ingress",
Namespace: "default",
},
}
builder.PreIngress.Meta.Annotations = map[string]string{
ClusterIdAnnotation: "pre-cluster",
}
dupDomain := builder.Build()
assert.Contains(t, dupDomain.Error, "conflicted with ingress")
builder.Protocol = HTTPS
builder.SecretName = "test-secret"
builder.Event = ""
httpsDomain := builder.Build()
assert.Equal(t, string(HTTPS), httpsDomain.Protocol)
assert.Equal(t, "test-secret", httpsDomain.SecretName)
}

View File

@@ -146,7 +146,7 @@ func GetHost(annotations map[string]string) string {
// Istio requires that the name of the gateway must conform to the DNS label.
// For details, you can view: https://github.com/istio/istio/blob/2d5c40ad5e9cceebe64106005aa38381097da2ba/pkg/config/validation/validation.go#L478
func convertToDNSLabelValid(input string) string {
func ConvertToDNSLabelValid(input string) string {
hasher := md5.New()
hasher.Write([]byte(input))
hash := hasher.Sum(nil)
@@ -156,7 +156,7 @@ func convertToDNSLabelValid(input string) string {
// CleanHost follow the format of mse-ops for host.
func CleanHost(host string) string {
return convertToDNSLabelValid(host)
return ConvertToDNSLabelValid(host)
}
func CreateConvertedName(items ...string) string {

View File

@@ -18,6 +18,7 @@ import (
"testing"
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pilot/pkg/model"
"istio.io/istio/pkg/config"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -556,3 +557,514 @@ func TestSortHTTPRoutesWithMoreRules(t *testing.T) {
}
}
}
func TestValidateBackendResource(t *testing.T) {
groupStr := "networking.higress.io"
testCases := []struct {
name string
resource *v1.TypedLocalObjectReference
expected bool
}{
{
name: "nil resource",
resource: nil,
expected: false,
},
{
name: "nil APIGroup",
resource: &v1.TypedLocalObjectReference{
APIGroup: nil,
Kind: "McpBridge",
Name: "default",
},
expected: false,
},
{
name: "wrong APIGroup",
resource: &v1.TypedLocalObjectReference{
APIGroup: &groupStr,
Kind: "McpBridge",
Name: "wrong-name",
},
expected: false,
},
{
name: "wrong Kind",
resource: &v1.TypedLocalObjectReference{
APIGroup: &groupStr,
Kind: "WrongKind",
Name: "default",
},
expected: false,
},
{
name: "valid resource",
resource: &v1.TypedLocalObjectReference{
APIGroup: &groupStr,
Kind: "McpBridge",
Name: "default",
},
expected: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := ValidateBackendResource(tc.resource)
assert.Equal(t, tc.expected, result)
})
}
}
func TestCreateOrUpdateAnnotations(t *testing.T) {
testCases := []struct {
name string
annotations map[string]string
options Options
expected map[string]string
}{
{
name: "empty annotations",
annotations: map[string]string{},
options: Options{
ClusterId: "test-cluster",
RawClusterId: "raw-test-cluster",
},
expected: map[string]string{
ClusterIdAnnotation: "test-cluster",
RawClusterIdAnnotation: "raw-test-cluster",
},
},
{
name: "existing annotations",
annotations: map[string]string{
"key1": "value1",
"key2": "value2",
},
options: Options{
ClusterId: "test-cluster",
RawClusterId: "raw-test-cluster",
},
expected: map[string]string{
"key1": "value1",
"key2": "value2",
ClusterIdAnnotation: "test-cluster",
RawClusterIdAnnotation: "raw-test-cluster",
},
},
{
name: "overwrite existing cluster annotations",
annotations: map[string]string{
ClusterIdAnnotation: "old-cluster",
RawClusterIdAnnotation: "old-raw-cluster",
"key1": "value1",
},
options: Options{
ClusterId: "new-cluster",
RawClusterId: "new-raw-cluster",
},
expected: map[string]string{
ClusterIdAnnotation: "new-cluster",
RawClusterIdAnnotation: "new-raw-cluster",
"key1": "value1",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := CreateOrUpdateAnnotations(tc.annotations, tc.options)
assert.Equal(t, tc.expected, result)
})
}
}
func TestGetClusterId(t *testing.T) {
testCases := []struct {
name string
annotations map[string]string
expected string
}{
{
name: "nil annotations",
annotations: nil,
expected: "",
},
{
name: "empty annotations",
annotations: map[string]string{},
expected: "",
},
{
name: "with cluster id",
annotations: map[string]string{
ClusterIdAnnotation: "test-cluster",
},
expected: "test-cluster",
},
{
name: "with other annotations",
annotations: map[string]string{
"key1": "value1",
"key2": "value2",
},
expected: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := GetClusterId(tc.annotations)
assert.Equal(t, tc.expected, string(result))
})
}
}
func TestConvertToDNSLabelValidAndCleanHost(t *testing.T) {
testCases := []struct {
name string
input string
}{
{
name: "simple host",
input: "example.com",
},
{
name: "wildcard host",
input: "*.example.com",
},
{
name: "long host",
input: "very-long-subdomain.example-service.my-namespace.svc.cluster.local",
},
{
name: "empty host",
input: "",
},
{
name: "ip address",
input: "192.168.1.1",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Test internal convertToDNSLabelValid function (through CleanHost)
result := CleanHost(tc.input)
// Validate result
assert.NotEmpty(t, result)
assert.Equal(t, 16, len(result)) // MD5 hash format is fixed length of 16 bytes
// Consistency check - same input should produce same output
result2 := CleanHost(tc.input)
assert.Equal(t, result, result2)
})
}
}
func TestSplitServiceFQDN(t *testing.T) {
testCases := []struct {
name string
fqdn string
expectedSvc string
expectedNs string
expectedValid bool
}{
{
name: "simple fqdn",
fqdn: "service.namespace",
expectedSvc: "service",
expectedNs: "namespace",
expectedValid: true,
},
{
name: "full k8s fqdn",
fqdn: "service.namespace.svc.cluster.local",
expectedSvc: "service",
expectedNs: "namespace",
expectedValid: true,
},
{
name: "just service name",
fqdn: "service",
expectedSvc: "",
expectedNs: "",
expectedValid: false,
},
{
name: "empty string",
fqdn: "",
expectedSvc: "",
expectedNs: "",
expectedValid: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
svc, ns, valid := SplitServiceFQDN(tc.fqdn)
assert.Equal(t, tc.expectedSvc, svc)
assert.Equal(t, tc.expectedNs, ns)
assert.Equal(t, tc.expectedValid, valid)
})
}
}
func TestConvertBackendService(t *testing.T) {
testCases := []struct {
name string
dest *networking.HTTPRouteDestination
expected model.BackendService
}{
{
name: "simple service",
dest: &networking.HTTPRouteDestination{
Destination: &networking.Destination{
Host: "service.namespace",
Port: &networking.PortSelector{
Number: 80,
},
},
Weight: 100,
},
expected: model.BackendService{
Name: "service",
Namespace: "namespace",
Port: 80,
Weight: 100,
},
},
{
name: "full k8s FQDN",
dest: &networking.HTTPRouteDestination{
Destination: &networking.Destination{
Host: "service.namespace.svc.cluster.local",
Port: &networking.PortSelector{
Number: 8080,
},
},
Weight: 50,
},
expected: model.BackendService{
Name: "service",
Namespace: "namespace",
Port: 8080,
Weight: 50,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := ConvertBackendService(tc.dest)
assert.Equal(t, tc.expected.Name, result.Name)
assert.Equal(t, tc.expected.Namespace, result.Namespace)
assert.Equal(t, tc.expected.Port, result.Port)
assert.Equal(t, tc.expected.Weight, result.Weight)
})
}
}
func TestCreateConvertedName(t *testing.T) {
testCases := []struct {
name string
items []string
expected string
}{
{
name: "empty slice",
items: []string{},
expected: "",
},
{
name: "single item",
items: []string{"example"},
expected: "example",
},
{
name: "multiple items",
items: []string{"part1", "part2", "part3"},
expected: "part1-part2-part3",
},
{
name: "with empty strings",
items: []string{"part1", "", "part3"},
expected: "part1-part3",
},
{
name: "all empty strings",
items: []string{"", "", ""},
expected: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := CreateConvertedName(tc.items...)
assert.Equal(t, tc.expected, result)
})
}
}
func TestSortIngressByCreationTime(t *testing.T) {
configs := []config.Config{
{
Meta: config.Meta{
Name: "c-ingress",
Namespace: "ns1",
},
},
{
Meta: config.Meta{
Name: "a-ingress",
Namespace: "ns1",
},
},
{
Meta: config.Meta{
Name: "b-ingress",
Namespace: "ns1",
},
},
}
expected := []string{"a-ingress", "b-ingress", "c-ingress"}
SortIngressByCreationTime(configs)
var actual []string
for _, cfg := range configs {
actual = append(actual, cfg.Name)
}
assert.Equal(t, expected, actual, "When the timestamps are the same, the configuration should be sorted by name")
sameNamespaceConfigs := []config.Config{
{
Meta: config.Meta{
Name: "same-name",
Namespace: "c-ns",
},
},
{
Meta: config.Meta{
Name: "same-name",
Namespace: "a-ns",
},
},
{
Meta: config.Meta{
Name: "same-name",
Namespace: "b-ns",
},
},
}
expectedNamespace := []string{"a-ns", "b-ns", "c-ns"}
SortIngressByCreationTime(sameNamespaceConfigs)
var actualNamespace []string
for _, cfg := range sameNamespaceConfigs {
actualNamespace = append(actualNamespace, cfg.Namespace)
}
assert.Equal(t, expectedNamespace, actualNamespace, "When the names are the same, the configuration should be sorted by namespace")
}
func TestPartMd5(t *testing.T) {
testCases := []struct {
name string
input string
length int
}{
{
name: "empty string",
input: "",
length: 8,
},
{
name: "simple string",
input: "test",
length: 8,
},
{
name: "complex string",
input: "this-is-a-long-string-with-special-chars-!@#$%^&*()",
length: 8,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := partMd5(tc.input)
// Check result format
assert.Equal(t, tc.length, len(result), "MD5 hash excerpt should be 8 characters")
// Run twice to ensure deterministic output
result2 := partMd5(tc.input)
assert.Equal(t, result, result2, "partMd5 function should be deterministic")
})
}
}
func TestGetLbStatusListV1AndV1Beta1(t *testing.T) {
clusterPrefix = "gw-123-"
svcName := clusterPrefix
svcList := []*v1.Service{
{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
},
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeLoadBalancer,
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: []v1.LoadBalancerIngress{
{
IP: "2.2.2.2",
},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
},
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeLoadBalancer,
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: []v1.LoadBalancerIngress{
{
Hostname: "1.1.1.1" + SvcHostNameSuffix,
},
},
},
},
},
}
// Test the V1 version
t.Run("GetLbStatusListV1", func(t *testing.T) {
lbiList := GetLbStatusListV1(svcList)
assert.Equal(t, 2, len(lbiList), "There should be 2 entry points")
assert.Equal(t, "1.1.1.1", lbiList[0].IP, "The first IP should be 1.1.1.1")
assert.Equal(t, "2.2.2.2", lbiList[1].IP, "The second IP should be 2.2.2.2")
})
// Test the V1Beta1 version
t.Run("GetLbStatusListV1Beta1", func(t *testing.T) {
lbiList := GetLbStatusListV1Beta1(svcList)
assert.Equal(t, 2, len(lbiList), "There should be 2 entry points")
assert.Equal(t, "1.1.1.1", lbiList[0].IP, "The first IP should be 1.1.1.1")
assert.Equal(t, "2.2.2.2", lbiList[1].IP, "The second IP should be 2.2.2.2")
})
}

View File

@@ -32,6 +32,7 @@ import (
"sigs.k8s.io/yaml"
"github.com/alibaba/higress/pkg/ingress/kube/controller"
"github.com/alibaba/higress/pkg/ingress/kube/mcpserver"
"github.com/alibaba/higress/pkg/ingress/kube/util"
. "github.com/alibaba/higress/pkg/ingress/log"
)
@@ -111,6 +112,14 @@ func (c *ConfigmapMgr) GetHigressConfig() *HigressConfig {
return nil
}
func (c *ConfigmapMgr) RegisterMcpServerProvider(provider mcpserver.McpServerProvider) {
for _, itemController := range c.ItemControllers {
if mcpRouteProviderAware, ok := itemController.(mcpserver.McpRouteProviderAware); ok {
mcpRouteProviderAware.RegisterMcpServerProvider(provider)
}
}
}
func (c *ConfigmapMgr) AddItemControllers(controllers ...ItemController) {
c.ItemControllers = append(c.ItemControllers, controllers...)
}
@@ -149,7 +158,7 @@ func (c *ConfigmapMgr) AddOrUpdateHigressConfig(name util.ClusterNamespacedName)
IngressLog.Infof("configmapMgr oldHigressConfig: %s", GetHigressConfigString(oldHigressConfig))
IngressLog.Infof("configmapMgr newHigressConfig: %s", GetHigressConfigString(newHigressConfig))
result, _ := c.CompareHigressConfig(oldHigressConfig, newHigressConfig)
IngressLog.Infof("configmapMgr CompareHigressConfig reuslt is %d", result)
IngressLog.Infof("configmapMgr CompareHigressConfig result is %d", result)
if result == ResultNothing {
return
@@ -168,7 +177,7 @@ func (c *ConfigmapMgr) AddOrUpdateHigressConfig(name util.ClusterNamespacedName)
}
}
c.SetHigressConfig(newHigressConfig)
IngressLog.Infof("configmapMgr higress config AddOrUpdate success, reuslt is %d", result)
IngressLog.Infof("configmapMgr higress config AddOrUpdate success, result is %d", result)
// Call updateConfig
}

View File

@@ -22,11 +22,13 @@ import (
"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"
"github.com/alibaba/higress/pkg/ingress/kube/mcpserver"
"github.com/alibaba/higress/pkg/ingress/kube/util"
. "github.com/alibaba/higress/pkg/ingress/log"
)
// RedisConfig defines the configuration for Redis connection
@@ -55,12 +57,14 @@ type MCPRatelimitConfig struct {
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)
// 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"`
// The domain list of the SSE server
DomainList []string `json:"domain_list,omitempty"`
}
// MatchRule defines a rule for matching requests
@@ -71,6 +75,12 @@ type MatchRule struct {
MatchRulePath string `json:"match_rule_path,omitempty"`
// Type of match rule: exact, prefix, suffix, contains, regex
MatchRuleType string `json:"match_rule_type,omitempty"`
// Type of upstream(s) matched by the rule: rest (default), sse
UpstreamType string `json:"upstream_type"`
// Enable request path rewrite for matched routes
EnablePathRewrite bool `json:"enable_path_rewrite"`
// Prefix the request path would be rewritten to.
PathRewritePrefix string `json:"path_rewrite_prefix"`
}
// McpServer defines the configuration for MCP (Model Context Protocol) server
@@ -80,7 +90,7 @@ type McpServer struct {
// 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"`
SSEPathSuffix string `json:"sse_path_suffix,omitempty"`
// List of SSE servers Configs
Servers []*SSEServer `json:"servers,omitempty"`
// List of match rules for filtering requests
@@ -115,21 +125,32 @@ func validMcpServer(m *McpServer) error {
// Validate match rule types
if m.MatchList != nil {
validTypes := map[string]bool{
validMatchRuleTypes := map[string]bool{
"exact": true,
"prefix": true,
"suffix": true,
"contains": true,
"regex": true,
}
validUpstreamTypes := map[string]bool{
"rest": true,
"sse": true,
"streamable": 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] {
if !validMatchRuleTypes[rule.MatchRuleType] {
return fmt.Errorf("invalid match_rule_type: %s, must be one of: exact, prefix, suffix, contains, regex", rule.MatchRuleType)
}
if rule.UpstreamType != "" && !validUpstreamTypes[rule.UpstreamType] {
return fmt.Errorf("invalid upstream_type: %s, must be one of: rest, sse, streamable", rule.UpstreamType)
}
if rule.EnablePathRewrite && rule.UpstreamType != "sse" {
return errors.New("path rewrite is only supported for SSE upstream type")
}
}
}
@@ -171,7 +192,7 @@ func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
WhiteList: mcp.Ratelimit.WhiteList,
}
}
newMcp.SsePathSuffix = mcp.SsePathSuffix
newMcp.SSEPathSuffix = mcp.SSEPathSuffix
newMcp.EnableUserLevelServer = mcp.EnableUserLevelServer
@@ -179,9 +200,10 @@ func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
newMcp.Servers = make([]*SSEServer, len(mcp.Servers))
for i, server := range mcp.Servers {
newServer := &SSEServer{
Name: server.Name,
Path: server.Path,
Type: server.Type,
Name: server.Name,
Path: server.Path,
Type: server.Type,
DomainList: server.DomainList,
}
if server.Config != nil {
newServer.Config = make(map[string]interface{})
@@ -197,9 +219,12 @@ func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
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,
MatchRuleDomain: rule.MatchRuleDomain,
MatchRulePath: rule.MatchRulePath,
MatchRuleType: rule.MatchRuleType,
UpstreamType: rule.UpstreamType,
EnablePathRewrite: rule.EnablePathRewrite,
PathRewritePrefix: rule.PathRewritePrefix,
}
}
}
@@ -208,17 +233,19 @@ func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
}
type McpServerController struct {
Namespace string
mcpServer atomic.Value
Name string
eventHandler ItemEventHandler
Namespace string
mcpServer atomic.Value
Name string
eventHandler ItemEventHandler
mcpServerProviders map[mcpserver.McpServerProvider]bool
}
func NewMcpServerController(namespace string) *McpServerController {
mcpController := &McpServerController{
Namespace: namespace,
mcpServer: atomic.Value{},
Name: "mcpServer",
Namespace: namespace,
Name: "mcpServer",
mcpServer: atomic.Value{},
mcpServerProviders: make(map[mcpserver.McpServerProvider]bool),
}
mcpController.SetMcpServer(NewDefaultMcpServer())
return mcpController
@@ -285,6 +312,13 @@ func (m *McpServerController) RegisterItemEventHandler(eventHandler ItemEventHan
m.eventHandler = eventHandler
}
func (m *McpServerController) RegisterMcpServerProvider(provider mcpserver.McpServerProvider) {
if m.mcpServerProviders == nil {
m.mcpServerProviders = make(map[mcpserver.McpServerProvider]bool)
}
m.mcpServerProviders[provider] = true
}
func (m *McpServerController) ConstructEnvoyFilters() ([]*config.Config, error) {
configs := make([]*config.Config, 0)
mcpServer := m.GetMcpServer()
@@ -294,88 +328,132 @@ func (m *McpServerController) ConstructEnvoyFilters() ([]*config.Config, error)
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",
// mcp-session envoy filter
mcpSessionStruct := m.constructMcpSessionStruct(mcpServer)
if mcpSessionStruct != "" {
sessionConfig := &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),
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_INSERT_AFTER,
Value: util.BuildPatchStruct(mcpSessionStruct),
},
},
},
},
},
}
configs = append(configs, sessionConfig)
}
// mcp-server envoy filter
mcpServerStruct := m.constructMcpServerStruct(mcpServer)
if mcpServerStruct != "" {
serverConfig := &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.EnvoyFilter,
Name: higressMcpServerEnvoyFilterName + "-server",
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.router",
},
},
},
},
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_INSERT_BEFORE,
Value: util.BuildPatchStruct(mcpServerStruct),
},
},
},
},
}
configs = append(configs, serverConfig)
}
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, ","))
}
func (m *McpServerController) constructMcpSessionStruct(mcp *McpServer) string {
// 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(`{
var matchList []*MatchRule
matchList = append(matchList, mcp.MatchList...)
for provider, _ := range m.mcpServerProviders {
servers := provider.GetMcpServers()
if len(servers) == 0 {
continue
}
for _, server := range servers {
matchRuleDomain := ""
if len(server.Domains) != 0 {
if len(server.Domains) > 1 {
matchRuleDomain = fmt.Sprintf("(%s)", strings.Join(server.Domains, "|"))
} else {
matchRuleDomain = server.Domains[0]
}
}
matchList = append(matchList, &MatchRule{
MatchRuleDomain: matchRuleDomain,
MatchRuleType: server.PathMatchType,
MatchRulePath: server.PathMatchValue,
UpstreamType: server.UpstreamType,
EnablePathRewrite: server.EnablePathRewrite,
PathRewritePrefix: server.PathRewritePrefix,
})
}
}
matchListConfig := "[]"
if len(matchList) > 0 {
matchConfigs := make([]string, 0, len(matchList))
for _, rule := range matchList {
matchConfigs = append(matchConfigs, fmt.Sprintf(`{
"match_rule_domain": "%s",
"match_rule_path": "%s",
"match_rule_type": "%s"
}`, rule.MatchRuleDomain, rule.MatchRulePath, rule.MatchRuleType)
"match_rule_type": "%s",
"upstream_type": "%s",
"enable_path_rewrite": %t,
"path_rewrite_prefix": "%s"
}`, rule.MatchRuleDomain, rule.MatchRulePath, rule.MatchRuleType, rule.UpstreamType, rule.EnablePathRewrite, rule.PathRewritePrefix))
}
matchList = fmt.Sprintf("[%s]", strings.Join(matchConfigs, ","))
matchListConfig = fmt.Sprintf("[%s]", strings.Join(matchConfigs, ","))
}
// 构建 Redis 配置
// Build redis configuration
redisConfig := "null"
if mcp.Redis != nil {
redisConfig = fmt.Sprintf(`{
@@ -386,7 +464,7 @@ func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
}`, mcp.Redis.Address, mcp.Redis.Username, mcp.Redis.Password, mcp.Redis.DB)
}
// 构建限流配置
// Build rate limit configuration
rateLimitConfig := "null"
if mcp.Ratelimit != nil {
whiteList := "[]"
@@ -417,7 +495,6 @@ func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
"rate_limit": %s,
"sse_path_suffix": "%s",
"match_list": %s,
"servers": %s,
"enable_user_level_server": %t
}
}
@@ -426,8 +503,60 @@ func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
}`,
redisConfig,
rateLimitConfig,
mcp.SsePathSuffix,
matchList,
servers,
mcp.SSEPathSuffix,
matchListConfig,
mcp.EnableUserLevelServer)
}
func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
// if no servers, return empty string
if mcp == nil || len(mcp.Servers) == 0 {
return ""
}
// 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.DomainList) > 0 {
domainList := fmt.Sprintf(`["%s"]`, strings.Join(server.DomainList, `","`))
serverConfig += fmt.Sprintf(`,
"domain_list": %s`, domainList)
}
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 complete configuration structure
return fmt.Sprintf(`{
"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/golang-filter.so",
"plugin_name": "mcp-server",
"plugin_config": {
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
"value": {
"servers": %s
}
}
}
}
}`, servers)
}

View File

@@ -15,6 +15,7 @@
package configmap
import (
"encoding/json"
"errors"
"testing"
@@ -53,6 +54,61 @@ func Test_validMcpServer(t *testing.T) {
},
wantErr: nil,
},
{
name: "enabled but bad match_rule_type",
mcp: &McpServer{
Enable: true,
EnableUserLevelServer: false,
Redis: nil,
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
MatchRulePath: "/mcp",
MatchRuleType: "bad-type",
},
},
Servers: []*SSEServer{},
},
wantErr: errors.New("invalid match_rule_type: bad-type, must be one of: exact, prefix, suffix, contains, regex"),
},
{
name: "enabled but bad upstream_type",
mcp: &McpServer{
Enable: true,
EnableUserLevelServer: false,
Redis: nil,
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
MatchRulePath: "/mcp",
MatchRuleType: "prefix",
UpstreamType: "bad-type",
},
},
Servers: []*SSEServer{},
},
wantErr: errors.New("invalid upstream_type: bad-type, must be one of: rest, sse, streamable"),
},
{
name: "enabled but path rewrite with unsupported upstream type",
mcp: &McpServer{
Enable: true,
EnableUserLevelServer: false,
Redis: nil,
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
MatchRulePath: "/mcp",
MatchRuleType: "prefix",
UpstreamType: "rest",
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
},
Servers: []*SSEServer{},
},
wantErr: errors.New("path rewrite is only supported for SSE upstream type"),
},
{
name: "enabled with user level server but no redis config",
mcp: &McpServer{
@@ -75,7 +131,7 @@ func Test_validMcpServer(t *testing.T) {
Password: "password",
DB: 0,
},
SsePathSuffix: "/sse",
SSEPathSuffix: "/sse",
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
@@ -237,7 +293,7 @@ func Test_deepCopyMcpServer(t *testing.T) {
Password: "password",
DB: 0,
},
SsePathSuffix: "/sse",
SSEPathSuffix: "/sse",
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
@@ -264,7 +320,7 @@ func Test_deepCopyMcpServer(t *testing.T) {
Password: "password",
DB: 0,
},
SsePathSuffix: "/sse",
SSEPathSuffix: "/sse",
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
@@ -422,3 +478,325 @@ func TestMcpServerController_AddOrUpdateHigressConfig(t *testing.T) {
})
}
}
func TestMcpServerController_ValidHigressConfig(t *testing.T) {
tests := []struct {
name string
higressConfig *HigressConfig
wantErr error
}{
{
name: "nil config",
higressConfig: nil,
wantErr: nil,
},
{
name: "nil mcp server",
higressConfig: &HigressConfig{
McpServer: nil,
},
wantErr: nil,
},
{
name: "valid config",
higressConfig: &HigressConfig{
McpServer: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "localhost:6379",
},
MatchList: []*MatchRule{},
Servers: []*SSEServer{},
},
},
wantErr: nil,
},
{
name: "invalid config - user level server without redis",
higressConfig: &HigressConfig{
McpServer: &McpServer{
Enable: true,
EnableUserLevelServer: true,
Redis: nil,
MatchList: []*MatchRule{},
Servers: []*SSEServer{},
},
},
wantErr: errors.New("redis config cannot be empty when user level server is enabled"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := NewMcpServerController("test-namespace")
err := m.ValidHigressConfig(tt.higressConfig)
assert.Equal(t, tt.wantErr, err)
})
}
}
func TestMcpServerController_ConstructEnvoyFilters(t *testing.T) {
tests := []struct {
name string
mcpServer *McpServer
wantConfigs int
wantErr error
}{
{
name: "nil mcp server",
mcpServer: nil,
wantConfigs: 0,
wantErr: nil,
},
{
name: "disabled mcp server",
mcpServer: &McpServer{
Enable: false,
},
wantConfigs: 0,
wantErr: nil,
},
{
name: "valid mcp server with redis",
mcpServer: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "localhost:6379",
},
MatchList: []*MatchRule{},
Servers: []*SSEServer{},
},
wantConfigs: 1, // Only session filter when no servers configured
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := NewMcpServerController("test-namespace")
m.mcpServer.Store(tt.mcpServer)
configs, err := m.ConstructEnvoyFilters()
assert.Equal(t, tt.wantErr, err)
assert.Equal(t, tt.wantConfigs, len(configs))
})
}
}
func TestMcpServerController_constructMcpSessionStruct(t *testing.T) {
tests := []struct {
name string
mcp *McpServer
wantJSON string
}{
{
name: "minimal config",
mcp: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "localhost:6379",
},
MatchList: []*MatchRule{},
Servers: []*SSEServer{},
},
wantJSON: `{
"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-session",
"library_path": "/var/lib/istio/envoy/golang-filter.so",
"plugin_name": "mcp-session",
"plugin_config": {
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
"value": {
"redis": {
"address": "localhost:6379",
"username": "",
"password": "",
"db": 0
},
"rate_limit": null,
"sse_path_suffix": "",
"match_list": [],
"enable_user_level_server": false
}
}
}
}
}`,
},
{
name: "full config",
mcp: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "localhost:6379",
Username: "user",
Password: "pass",
DB: 1,
},
SSEPathSuffix: "/sse",
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
MatchRulePath: "/test",
MatchRuleType: "exact",
},
{
MatchRuleDomain: "*",
MatchRulePath: "/sse-test-1",
MatchRuleType: "prefix",
UpstreamType: "sse",
},
{
MatchRuleDomain: "*",
MatchRulePath: "/sse-test-2",
MatchRuleType: "prefix",
UpstreamType: "sse",
EnablePathRewrite: true,
PathRewritePrefix: "/mcp",
},
},
EnableUserLevelServer: true,
Ratelimit: &MCPRatelimitConfig{
Limit: 100,
Window: 3600,
WhiteList: []string{"user1", "user2"},
},
},
wantJSON: `{
"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-session",
"library_path": "/var/lib/istio/envoy/golang-filter.so",
"plugin_name": "mcp-session",
"plugin_config": {
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
"value": {
"redis": {
"address": "localhost:6379",
"username": "user",
"password": "pass",
"db": 1
},
"rate_limit": {
"limit": 100,
"window": 3600,
"white_list": ["user1","user2"]
},
"sse_path_suffix": "/sse",
"match_list": [{
"match_rule_domain": "*",
"match_rule_path": "/test",
"match_rule_type": "exact",
"upstream_type": "",
"enable_path_rewrite": false,
"path_rewrite_prefix": ""
},{
"match_rule_domain": "*",
"match_rule_path": "/sse-test-1",
"match_rule_type": "prefix",
"upstream_type": "sse",
"enable_path_rewrite": false,
"path_rewrite_prefix": ""
},{
"match_rule_domain": "*",
"match_rule_path": "/sse-test-2",
"match_rule_type": "prefix",
"upstream_type": "sse",
"enable_path_rewrite": true,
"path_rewrite_prefix": "/mcp"
}],
"enable_user_level_server": true
}
}
}
}
}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := NewMcpServerController("test-namespace")
got := m.constructMcpSessionStruct(tt.mcp)
// Normalize JSON strings for comparison
var gotJSON, wantJSON interface{}
json.Unmarshal([]byte(got), &gotJSON)
json.Unmarshal([]byte(tt.wantJSON), &wantJSON)
assert.Equal(t, wantJSON, gotJSON)
})
}
}
func TestMcpServerController_constructMcpServerStruct(t *testing.T) {
tests := []struct {
name string
mcp *McpServer
wantJSON string
}{
{
name: "no servers",
mcp: &McpServer{
Servers: []*SSEServer{},
},
wantJSON: "", // Return empty string when no servers configured
},
{
name: "with servers",
mcp: &McpServer{
Servers: []*SSEServer{
{
Name: "test-server",
Path: "/test",
Type: "test",
Config: map[string]interface{}{
"key": "value",
},
DomainList: []string{"example.com"},
},
},
},
wantJSON: `{
"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/golang-filter.so",
"plugin_name": "mcp-server",
"plugin_config": {
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
"value": {
"servers": [{
"name": "test-server",
"path": "/test",
"type": "test",
"domain_list": ["example.com"],
"config": {"key":"value"}
}]
}
}
}
}
}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := NewMcpServerController("test-namespace")
got := m.constructMcpServerStruct(tt.mcp)
// Normalize JSON strings for comparison
var gotJSON, wantJSON interface{}
json.Unmarshal([]byte(got), &gotJSON)
json.Unmarshal([]byte(tt.wantJSON), &wantJSON)
assert.Equal(t, wantJSON, gotJSON)
})
}
}

View File

@@ -286,7 +286,7 @@ func testConvertHTTPRoute(t *testing.T, c common.KIngressController) {
expectNoError: true,
},
{
description: "valid httpRoute convention, vaild ingress",
description: "valid httpRoute convention, valid ingress",
input: struct {
options *common.ConvertOptions
wrapperConfig *common.WrapperConfig

View File

@@ -0,0 +1,60 @@
// Copyright (c) 2025 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 mcpserver
import (
"istio.io/istio/pkg/config"
)
var (
GvkMcpServer = config.GroupVersionKind{Group: "networking.higress.io", Version: "v1alpha1", Kind: "McpServer"}
)
const (
UpstreamTypeRest string = "rest"
UpstreamTypeSSE string = "sse"
UpstreamTypeStreamable string = "streamable"
ExactMatchType string = "exact"
PrefixMatchType string = "prefix"
SuffixMatchType string = "suffix"
ContainsMatchType string = "contains"
RegexMatchType string = "regex"
)
var (
ValidUpstreamTypes = map[string]bool{
UpstreamTypeRest: true,
UpstreamTypeSSE: true,
UpstreamTypeStreamable: true,
}
ValidPathMatchTypes = map[string]bool{
ExactMatchType: true,
PrefixMatchType: true,
SuffixMatchType: true,
ContainsMatchType: true,
RegexMatchType: true,
}
)
type McpServer struct {
Name string `json:"name,omitempty"`
Domains []string `json:"domains,omitempty"`
PathMatchType string `json:"path_match_type,omitempty"`
PathMatchValue string `json:"path_match_value,omitempty"`
UpstreamType string `json:"upstream_type,omitempty"`
EnablePathRewrite bool `json:"enable_path_rewrite,omitempty"`
PathRewritePrefix string `json:"path_rewrite_prefix,omitempty"`
}

View File

@@ -0,0 +1,70 @@
// Copyright (c) 2025 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 mcpserver
import (
"reflect"
"slices"
"strings"
"sync"
)
type McpServerProvider interface {
GetMcpServers() []*McpServer
}
type McpRouteProviderAware interface {
RegisterMcpServerProvider(provider McpServerProvider)
}
type McpServerCache struct {
mcpServers []*McpServer
mutex sync.RWMutex
}
func (c *McpServerCache) GetMcpServers() []*McpServer {
c.mutex.RLock()
defer c.mutex.RUnlock()
return c.mcpServers
}
// SetMcpServers sets the mcp servers and returns true if the cached list is changed
func (c *McpServerCache) SetMcpServers(mcpServers []*McpServer) bool {
c.mutex.Lock()
defer c.mutex.Unlock()
sortedMcpServers := make([]*McpServer, 0, len(mcpServers))
sortedMcpServers = append(sortedMcpServers, mcpServers...)
// Sort the mcp servers by PathMatchValue in descending order
slices.SortFunc(sortedMcpServers, func(a, b *McpServer) int {
return strings.Compare(a.Name, b.Name)
})
if len(c.mcpServers) == len(sortedMcpServers) {
changed := false
for i := range c.mcpServers {
if !reflect.DeepEqual(c.mcpServers[i], sortedMcpServers[i]) {
changed = true
break
}
}
if !changed {
return false
}
}
c.mcpServers = sortedMcpServers
return true
}

View File

@@ -0,0 +1,654 @@
// Copyright (c) 2025 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 mcpserver
import (
"testing"
"github.com/google/go-cmp/cmp"
)
func TestMcpServerCache_GetSet(t *testing.T) {
testCases := []struct {
name string
skip bool
init []*McpServer
input []*McpServer
expect []*McpServer
changed bool
}{
{
name: "nil",
init: nil,
input: nil,
changed: false,
expect: nil,
},
{
name: "nil to non-nil",
init: nil,
input: []*McpServer{
{
Name: "test2",
Domains: []string{"www.foo.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test2",
UpstreamType: UpstreamTypeSSE,
EnablePathRewrite: true,
PathRewritePrefix: "/test",
},
{
Name: "test3",
Domains: []string{"www.bar.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test3",
UpstreamType: UpstreamTypeStreamable,
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
{
Name: "test1",
Domains: nil,
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test1",
UpstreamType: UpstreamTypeRest,
EnablePathRewrite: false,
PathRewritePrefix: "",
},
},
changed: true,
expect: []*McpServer{
{
Name: "test1",
Domains: nil,
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test1",
UpstreamType: UpstreamTypeRest,
EnablePathRewrite: false,
PathRewritePrefix: "",
},
{
Name: "test2",
Domains: []string{"www.foo.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test2",
UpstreamType: UpstreamTypeSSE,
EnablePathRewrite: true,
PathRewritePrefix: "/test",
},
{
Name: "test3",
Domains: []string{"www.bar.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test3",
UpstreamType: UpstreamTypeStreamable,
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
},
},
{
name: "non-nil to non-nil (length increase)",
init: []*McpServer{
{
Name: "test1",
Domains: nil,
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test1",
UpstreamType: UpstreamTypeRest,
EnablePathRewrite: false,
PathRewritePrefix: "",
},
{
Name: "test2",
Domains: []string{"www.foo.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test2",
UpstreamType: UpstreamTypeSSE,
EnablePathRewrite: true,
PathRewritePrefix: "/test",
},
},
input: []*McpServer{
{
Name: "test2",
Domains: []string{"www.foo.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test2",
UpstreamType: UpstreamTypeSSE,
EnablePathRewrite: true,
PathRewritePrefix: "/test",
},
{
Name: "test3",
Domains: []string{"www.bar.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test3",
UpstreamType: UpstreamTypeStreamable,
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
{
Name: "test1",
Domains: nil,
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test1",
UpstreamType: UpstreamTypeRest,
EnablePathRewrite: false,
PathRewritePrefix: "",
},
},
changed: true,
expect: []*McpServer{
{
Name: "test1",
Domains: nil,
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test1",
UpstreamType: UpstreamTypeRest,
EnablePathRewrite: false,
PathRewritePrefix: "",
},
{
Name: "test2",
Domains: []string{"www.foo.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test2",
UpstreamType: UpstreamTypeSSE,
EnablePathRewrite: true,
PathRewritePrefix: "/test",
},
{
Name: "test3",
Domains: []string{"www.bar.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test3",
UpstreamType: UpstreamTypeStreamable,
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
},
},
{
name: "non-nil to non-nil (length decrease)",
init: []*McpServer{
{
Name: "test2",
Domains: []string{"www.foo.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test2",
UpstreamType: UpstreamTypeSSE,
EnablePathRewrite: true,
PathRewritePrefix: "/test",
},
{
Name: "test3",
Domains: []string{"www.bar.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test3",
UpstreamType: UpstreamTypeStreamable,
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
{
Name: "test1",
Domains: nil,
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test1",
UpstreamType: UpstreamTypeRest,
EnablePathRewrite: false,
PathRewritePrefix: "",
},
},
input: []*McpServer{
{
Name: "test3",
Domains: []string{"www.bar.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test3",
UpstreamType: UpstreamTypeStreamable,
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
{
Name: "test2",
Domains: []string{"www.foo.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test2",
UpstreamType: UpstreamTypeSSE,
EnablePathRewrite: true,
PathRewritePrefix: "/test",
},
},
changed: true,
expect: []*McpServer{
{
Name: "test2",
Domains: []string{"www.foo.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test2",
UpstreamType: UpstreamTypeSSE,
EnablePathRewrite: true,
PathRewritePrefix: "/test",
},
{
Name: "test3",
Domains: []string{"www.bar.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test3",
UpstreamType: UpstreamTypeStreamable,
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
},
},
{
name: "non-nil to non-nil (length unchanged + name field changed)",
init: []*McpServer{
{
Name: "test2",
Domains: []string{"www.foo.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test2",
UpstreamType: UpstreamTypeSSE,
EnablePathRewrite: true,
PathRewritePrefix: "/test",
},
{
Name: "test3",
Domains: []string{"www.bar.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test3",
UpstreamType: UpstreamTypeStreamable,
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
{
Name: "test1",
Domains: nil,
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test1",
UpstreamType: UpstreamTypeRest,
EnablePathRewrite: false,
PathRewritePrefix: "",
},
},
input: []*McpServer{
{
Name: "test2",
Domains: []string{"www.foo.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test2",
UpstreamType: UpstreamTypeSSE,
EnablePathRewrite: true,
PathRewritePrefix: "/test",
},
{
Name: "test3-1",
Domains: []string{"www.bar.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test3",
UpstreamType: UpstreamTypeStreamable,
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
{
Name: "test1",
Domains: nil,
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test1",
UpstreamType: UpstreamTypeRest,
EnablePathRewrite: false,
PathRewritePrefix: "",
},
},
changed: true,
expect: []*McpServer{
{
Name: "test1",
Domains: nil,
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test1",
UpstreamType: UpstreamTypeRest,
EnablePathRewrite: false,
PathRewritePrefix: "",
},
{
Name: "test2",
Domains: []string{"www.foo.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test2",
UpstreamType: UpstreamTypeSSE,
EnablePathRewrite: true,
PathRewritePrefix: "/test",
},
{
Name: "test3-1",
Domains: []string{"www.bar.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test3",
UpstreamType: UpstreamTypeStreamable,
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
},
},
{
name: "non-nil to non-nil (length unchanged + non-name field changed)",
init: []*McpServer{
{
Name: "test2",
Domains: []string{"www.foo.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test2",
UpstreamType: UpstreamTypeSSE,
EnablePathRewrite: true,
PathRewritePrefix: "/test",
},
{
Name: "test3",
Domains: []string{"www.bar.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test3",
UpstreamType: UpstreamTypeStreamable,
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
{
Name: "test1",
Domains: nil,
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test1",
UpstreamType: UpstreamTypeRest,
EnablePathRewrite: false,
PathRewritePrefix: "",
},
},
input: []*McpServer{
{
Name: "test2",
Domains: []string{"www.foo.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test2",
UpstreamType: UpstreamTypeSSE,
EnablePathRewrite: true,
PathRewritePrefix: "/test",
},
{
Name: "test3",
Domains: []string{"www.bar-2.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test4",
UpstreamType: UpstreamTypeStreamable,
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
{
Name: "test1",
Domains: nil,
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test1",
UpstreamType: UpstreamTypeRest,
EnablePathRewrite: false,
PathRewritePrefix: "",
},
},
changed: true,
expect: []*McpServer{
{
Name: "test1",
Domains: nil,
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test1",
UpstreamType: UpstreamTypeRest,
EnablePathRewrite: false,
PathRewritePrefix: "",
},
{
Name: "test2",
Domains: []string{"www.foo.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test2",
UpstreamType: UpstreamTypeSSE,
EnablePathRewrite: true,
PathRewritePrefix: "/test",
},
{
Name: "test3",
Domains: []string{"www.bar-2.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test4",
UpstreamType: UpstreamTypeStreamable,
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
},
},
{
name: "non-nil to non-nil (content unchanged + order unchanged)",
init: []*McpServer{
{
Name: "test2",
Domains: []string{"www.foo.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test2",
UpstreamType: UpstreamTypeSSE,
EnablePathRewrite: true,
PathRewritePrefix: "/test",
},
{
Name: "test3",
Domains: []string{"www.bar.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test3",
UpstreamType: UpstreamTypeStreamable,
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
{
Name: "test1",
Domains: nil,
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test1",
UpstreamType: UpstreamTypeRest,
EnablePathRewrite: false,
PathRewritePrefix: "",
},
},
input: []*McpServer{
{
Name: "test2",
Domains: []string{"www.foo.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test2",
UpstreamType: UpstreamTypeSSE,
EnablePathRewrite: true,
PathRewritePrefix: "/test",
},
{
Name: "test3",
Domains: []string{"www.bar.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test3",
UpstreamType: UpstreamTypeStreamable,
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
{
Name: "test1",
Domains: nil,
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test1",
UpstreamType: UpstreamTypeRest,
EnablePathRewrite: false,
PathRewritePrefix: "",
},
},
changed: false,
expect: []*McpServer{
{
Name: "test1",
Domains: nil,
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test1",
UpstreamType: UpstreamTypeRest,
EnablePathRewrite: false,
PathRewritePrefix: "",
},
{
Name: "test2",
Domains: []string{"www.foo.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test2",
UpstreamType: UpstreamTypeSSE,
EnablePathRewrite: true,
PathRewritePrefix: "/test",
},
{
Name: "test3",
Domains: []string{"www.bar.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test3",
UpstreamType: UpstreamTypeStreamable,
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
},
},
{
name: "non-nil to non-nil (content unchanged + order changed)",
init: []*McpServer{
{
Name: "test2",
Domains: []string{"www.foo.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test2",
UpstreamType: UpstreamTypeSSE,
EnablePathRewrite: true,
PathRewritePrefix: "/test",
},
{
Name: "test3",
Domains: []string{"www.bar.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test3",
UpstreamType: UpstreamTypeStreamable,
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
{
Name: "test1",
Domains: nil,
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test1",
UpstreamType: UpstreamTypeRest,
EnablePathRewrite: false,
PathRewritePrefix: "",
},
},
input: []*McpServer{
{
Name: "test3",
Domains: []string{"www.bar.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test3",
UpstreamType: UpstreamTypeStreamable,
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
{
Name: "test1",
Domains: nil,
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test1",
UpstreamType: UpstreamTypeRest,
EnablePathRewrite: false,
PathRewritePrefix: "",
},
{
Name: "test2",
Domains: []string{"www.foo.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test2",
UpstreamType: UpstreamTypeSSE,
EnablePathRewrite: true,
PathRewritePrefix: "/test",
},
},
changed: false,
expect: []*McpServer{
{
Name: "test1",
Domains: nil,
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test1",
UpstreamType: UpstreamTypeRest,
EnablePathRewrite: false,
PathRewritePrefix: "",
},
{
Name: "test2",
Domains: []string{"www.foo.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test2",
UpstreamType: UpstreamTypeSSE,
EnablePathRewrite: true,
PathRewritePrefix: "/test",
},
{
Name: "test3",
Domains: []string{"www.bar.com"},
PathMatchType: ExactMatchType,
PathMatchValue: "/mcp/test3",
UpstreamType: UpstreamTypeStreamable,
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
},
},
}
for _, tt := range testCases {
if tt.skip {
continue
}
t.Run(tt.name, func(t *testing.T) {
provider := &McpServerCache{}
if provider.GetMcpServers() != nil {
t.Fatalf("GetMcpServers doesn't return nil before testing.")
}
_ = provider.SetMcpServers(tt.init)
changed := provider.SetMcpServers(tt.input)
if changed != tt.changed {
t.Fatalf("actual changed %t != expect changed %t", changed, tt.changed)
return
}
actual := provider.GetMcpServers()
if len(actual) != len(tt.expect) {
t.Fatalf("actual length %d != expect length %d", len(actual), len(tt.expect))
}
for i := range actual {
if diff := cmp.Diff(tt.expect[i], actual[i]); diff != "" {
t.Fatalf("TestMcpServerCache_GetSet() mismatch (-want +got):\n%s", diff)
}
}
})
}
}

View File

@@ -1,4 +1,4 @@
FROM golang:1.23-bullseye AS golang-base
FROM golang:1.22-bullseye AS golang-base
ARG GOPROXY
ARG GO_FILTER_NAME
@@ -17,20 +17,20 @@ RUN if [ "$GOARCH" = "arm64" ]; then \
else \
echo "Installing AMD64 toolchain" && \
apt-get update && \
apt-get install -y gcc binutils; \
apt-get install -y gcc-x86-64-linux-gnu binutils-x86-64-linux-gnu; \
fi
WORKDIR /workspace
COPY . .
WORKDIR /workspace/$GO_FILTER_NAME
WORKDIR /workspace
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 .; \
CC=x86_64-linux-gnu-gcc AS=x86_64-linux-gnu-as go build -o /$GO_FILTER_NAME.so -buildmode=c-shared .; \
fi
FROM scratch AS output

View File

@@ -1,4 +1,4 @@
GO_FILTER_NAME ?= mcp-server
GO_FILTER_NAME ?= golang-filter
GOPROXY := $(shell go env GOPROXY)
GOARCH ?= amd64
@@ -8,5 +8,5 @@ build:
--build-arg GO_FILTER_NAME=${GO_FILTER_NAME} \
--build-arg GOARCH=${GOARCH} \
-t ${GO_FILTER_NAME} \
--output ./${GO_FILTER_NAME} \
--output . \
.

View File

@@ -20,28 +20,49 @@ Golang HTTP Filter 允许开发者使用 Go 语言编写自定义的 Envoy Filte
请参考 [Envoy Golang HTTP Filter 示例](https://github.com/envoyproxy/examples/tree/main/golang-http) 了解如何开发和运行一个基本的 Golang Filter。
## 插件注册
在开发新的 Golang Filter 时,需要在`main.go``init()` 函数中注册你的插件。注册时需要提供插件名称、Filter 工厂函数和配置解析器:
```go
func init() {
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(
"your-plugin-name", // 插件名称
yourFilterFactory, // Filter 工厂函数
&yourConfigParser{}, // 配置解析器
)
}
```
## 配置示例
多个 Golang Filter 插件可以共同编译到一个 `golang-filter.so` 文件中,通过 `plugin_name` 来指定要使用的插件。配置示例如下:
```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
library_id: your-plugin-name
library_path: "./golang-filter.so" # 包含多个插件的共享库文件
plugin_name: your-plugin-name # 指定要使用的插件名称,需要与 init() 函数中注册的插件名称保持一致
plugin_config:
"@type": type.googleapis.com/xds.type.v3.TypedStruct
value:
your_config_here: value
```
## 快速构建
使用以下命令可以快速构建 golang filter 插件:
使用以下命令可以快速构建 golang filter 插件
```bash
GO_FILTER_NAME=mcp-server make build
make build
```
如果是 arm64 架构,请设置 `GOARCH=arm64`
```bash
make build GOARCH=arm64
```
你也可以直接在 Higress 项目的根目录下执行 `make build-gateway-local` 来构建 Higress Gateway 镜像,`golang-filter.so` 将会自动构建并复制到镜像中。

View File

@@ -20,16 +20,32 @@ The Golang HTTP Filter allows developers to write custom Envoy Filters using the
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.
## Plugin Registration
When developing a new Golang Filter, you need to register your plugin in the `init()` function of `main.go`. The registration requires a plugin name, Filter factory function, and configuration parser:
```go
func init() {
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(
"your-plugin-name", // Plugin name
yourFilterFactory, // Filter factory function
&yourConfigParser{}, // Configuration parser
)
}
```
## Configuration Example
Multiple Golang Filter plugins can be compiled into a single `golang-filter.so` file, and the desired plugin can be specified using `plugin_name`. Here's an example configuration:
```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
library_id: your-plugin-name
library_path: "./golang-filter.so" # Shared library file containing multiple plugins
plugin_name: your-plugin-name # Specify which plugin to use, must match the name registered in init()
plugin_config:
"@type": type.googleapis.com/xds.type.v3.TypedStruct
value:
@@ -41,5 +57,13 @@ http_filters:
Use the following command to quickly build the golang filter plugin:
```bash
GO_FILTER_NAME=mcp-server make build
```
make build
```
If you are on an arm64 architecture, please set `GOARCH=arm64`:
```bash
make build GOARCH=arm64
```
Alternatively, you can build the Higress Gateway image directly by running `make build-gateway-local` in the root directory of the Higress project. The `golang-filter.so` file will be automatically built and included in the image.

View File

@@ -1,6 +1,10 @@
module github.com/alibaba/higress/plugins/golang-filter/mcp-server
module github.com/alibaba/higress/plugins/golang-filter
go 1.23
go 1.22
replace github.com/envoyproxy/envoy => github.com/higress-group/envoy v0.0.0-20250430151331-2c556780b65c
replace github.com/mark3labs/mcp-go => github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30
require (
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42

View File

@@ -136,8 +136,6 @@ github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9r
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.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/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=
@@ -236,6 +234,10 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO
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/higress-group/envoy v0.0.0-20250430151331-2c556780b65c h1:chAOZk/qEXFhLILWoNucj3X6r9xYnRR+SWFvhsOa2oo=
github.com/higress-group/envoy v0.0.0-20250430151331-2c556780b65c/go.mod h1:SU+IJUAfh1kkZtH+u0E1dnwho8AhbGeYMgp5vvjU+Gc=
github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30 h1:N4NMq8M1nZyyChPyzn+EUUdHi5asig2uLR5hOyRmsXI=
github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30/go.mod h1:O9gri9UOzthw728vusc2oNu99lVh8cKCajpxNfC90gE=
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=
@@ -283,8 +285,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-40 h1:nzRTBplC0riQqQwEHZThw5H4/TH5LgWTQTm6A7t1lpY=
github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-40/go.mod h1:9FKXl6FqOiVmm72i8kADtbeK71egyG9y3uRDBg41tpQ=
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=

View File

@@ -0,0 +1,25 @@
package main
import (
"net/http"
mcp_server "github.com/alibaba/higress/plugins/golang-filter/mcp-server"
mcp_session "github.com/alibaba/higress/plugins/golang-filter/mcp-session"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
envoyHttp "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http"
)
func init() {
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(mcp_session.Name, mcp_session.FilterFactory, &mcp_session.Parser{})
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(mcp_server.Name, mcp_server.FilterFactory, &mcp_server.Parser{})
go func() {
defer func() {
if r := recover(); r != nil {
api.LogErrorf("PProf server recovered from panic: %v", r)
}
}()
api.LogError(http.ListenAndServe("localhost:6060", nil).Error())
}()
}
func main() {}

View File

@@ -3,27 +3,22 @@
## 概述
MCP Server 是一个基于 Envoy 的 Golang Filter 插件,用于实现服务器端事件SSE和消息通信功能。该插件支持多种数据库类型并使用 Redis 作为消息队列来实现负载均衡的请求通过对应的SSE连接发送。
MCP Server 是一个基于 Envoy 的 Golang Filter 插件,提供了统一的 MCP (Model Context Protocol) 服务接口。它支持多种后端服务的集成,包括:
> **注意**MCP Server需要 Higress 2.1.0 或更高版本才能使用。
## 项目结构
```
mcp-server/
├── config.go # 配置解析相关代码
├── filter.go # 请求处理相关代码
├── internal/ # 内部实现逻辑
├── servers/ # MCP 服务器实现
├── go.mod # Go模块依赖定义
└── go.sum # Go模块依赖校验
```
## MCP Server开发指南
- 数据库服务:通过 GORM 支持多种数据库的访问和管理
- 配置中心:支持 Nacos 配置中心的集成
- 可扩展性:支持自定义服务器实现,方便集成其他服务
> **注意**MCP Server 需要 Higress 2.1.0 或更高版本才能使用。
## MCP Server 开发指南
```go
// 在init函数中注册你的服务器
// 参数1: 服务器名称
// 参数2: 配置结构体实例
func init() {
internal.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
common.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
}
// 服务器配置结构体
@@ -43,8 +38,8 @@ func (c *DBConfig) ParseConfig(config map[string]any) error {
// 创建新的MCP服务器实例
// serverName: 服务器名称
// 返回值: MCP服务器实例和可能的错误
func (c *DBConfig) NewServer(serverName string) (*internal.MCPServer, error) {
mcpServer := internal.NewMCPServer(serverName, Version)
func (c *DBConfig) NewServer(serverName string) (*common.MCPServer, error) {
mcpServer := common.NewMCPServer(serverName, Version)
// 添加工具方法到服务器
// mcpServer.AddTool()

View File

@@ -3,29 +3,22 @@ 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.
MCP Server is a Golang Filter plugin based on Envoy that provides a unified MCP (Model Context Protocol) service interface. It supports integration with various backend services, including:
> **Note**: MCP Server requires Higress 2.1.0 or higher version.
- Database Services: Supports multiple database access and management through GORM
- Configuration Service: Supports integration with Nacos configuration service
- Extensibility: Supports custom server implementations for easy integration with other services
## 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
```
> **Note**: MCP Server requires Higress version 2.1.0 or higher to be used.
## MCP Server Development Guide
```go
// Register your server in the init function
// Param 1: Server name
// Param 2: Config struct instance
// Parameter 1: Server name
// Parameter 2: Configuration struct instance
func init() {
internal.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
common.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
}
// Server configuration struct
@@ -33,7 +26,7 @@ type DemoConfig struct {
helloworld string
}
// Configuration parsing method
// Parse configuration method
// Parse and validate configuration items from the config map
func (c *DBConfig) ParseConfig(config map[string]any) error {
helloworld, ok := config["helloworld"].(string)
@@ -45,13 +38,13 @@ func (c *DBConfig) ParseConfig(config map[string]any) error {
// 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)
func (c *DBConfig) NewServer(serverName string) (*common.MCPServer, error) {
mcpServer := common.NewMCPServer(serverName, Version)
// Add tool methods to server
// Add tool methods to the server
// mcpServer.AddTool()
// Add resources to server
// Add resources to the server
// mcpServer.AddResource()
return mcpServer, nil
@@ -59,7 +52,7 @@ func (c *DBConfig) NewServer(serverName string) (*internal.MCPServer, error) {
```
**Note**:
Need to use underscore import in config.go to execute the package's init function
You need to use underscore imports 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

@@ -1,64 +1,40 @@
package main
package mcp_server
import (
"fmt"
"net/http"
_ "net/http/pprof"
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/handler"
"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/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress/higress-api"
mcp_session "github.com/alibaba/higress/plugins/golang-filter/mcp-session"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
xds "github.com/cncf/xds/go/xds/type/v3"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
envoyHttp "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http"
"google.golang.org/protobuf/types/known/anypb"
)
const Name = "mcp-session"
const Name = "mcp-server"
const Version = "1.0.0"
const DefaultServerName = "defaultServer"
const ConfigPathSuffix = "/config"
func init() {
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(Name, filterFactory, &parser{})
go func() {
defer func() {
if r := recover(); r != nil {
api.LogErrorf("PProf server recovered from panic: %v", r)
}
}()
api.LogError(http.ListenAndServe("localhost:6060", nil).Error())
}()
type SSEServerWrapper struct {
BaseServer *common.SSEServer
DomainList []string
}
type config struct {
ssePathSuffix string
redisClient *internal.RedisClient
servers []*internal.SSEServer
defaultServer *internal.SSEServer
matchList []internal.MatchRule
enableUserLevelServer bool
rateLimitConfig *handler.MCPRatelimitConfig
servers []*SSEServerWrapper
}
func (c *config) Destroy() {
if c.redisClient != nil {
api.LogDebug("Closing Redis client")
c.redisClient.Close()
}
for _, server := range c.servers {
server.Close()
server.BaseServer.Close()
}
}
type parser struct {
type Parser struct {
}
// Parse the filter configuration
func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (interface{}, error) {
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
@@ -66,82 +42,9 @@ func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
v := configStruct.Value
conf := &config{
matchList: make([]internal.MatchRule, 0),
servers: make([]*internal.SSEServer, 0),
servers: make([]*SSEServerWrapper, 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)
}
}
}
// Redis configuration is optional
if redisConfigMap, ok := v.AsMap()["redis"].(map[string]interface{}); ok {
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
api.LogDebug("Redis client initialized")
} else {
api.LogDebug("Redis configuration not provided, running without Redis")
}
enableUserLevelServer, ok := v.AsMap()["enable_user_level_server"].(bool)
if !ok {
enableUserLevelServer = false
if conf.redisClient == nil {
return nil, fmt.Errorf("redis configuration is not provided, enable_user_level_server is true")
}
}
conf.enableUserLevelServer = enableUserLevelServer
if rateLimit, ok := v.AsMap()["rate_limit"].(map[string]interface{}); ok {
rateLimitConfig := &handler.MCPRatelimitConfig{}
if limit, ok := rateLimit["limit"].(float64); ok {
rateLimitConfig.Limit = int(limit)
}
if window, ok := rateLimit["window"].(float64); ok {
rateLimitConfig.Window = int(window)
}
if whiteList, ok := rateLimit["white_list"].([]interface{}); ok {
for _, item := range whiteList {
if uid, ok := item.(string); ok {
rateLimitConfig.Whitelist = append(rateLimitConfig.Whitelist, uid)
}
}
}
if errorText, ok := rateLimit["error_text"].(string); ok {
rateLimitConfig.ErrorText = errorText
}
conf.rateLimitConfig = rateLimitConfig
}
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")
@@ -153,19 +56,33 @@ func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
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)
}
serverDomainList := []string{}
if domainList, ok := serverConfigMap["domain_list"].([]interface{}); ok {
for _, domain := range domainList {
if domainStr, ok := domain.(string); ok {
serverDomainList = append(serverDomainList, domainStr)
}
}
} else {
serverDomainList = []string{"*"}
}
serverName, ok := serverConfigMap["name"].(string)
if !ok {
return nil, fmt.Errorf("server %s name is not set", serverType)
}
server := internal.GlobalRegistry.GetServer(serverType)
server := common.GlobalRegistry.GetServer(serverType)
if server == nil {
return nil, fmt.Errorf("server %s is not registered", serverType)
@@ -183,53 +100,39 @@ func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
serverInstance, err := server.NewServer(serverName)
if err != nil {
return nil, fmt.Errorf("failed to initialize DBServer: %w", err)
return nil, fmt.Errorf("failed to initialize MCP Server: %w", err)
}
conf.servers = append(conf.servers, internal.NewSSEServer(serverInstance,
internal.WithRedisClient(conf.redisClient),
internal.WithSSEEndpoint(fmt.Sprintf("%s%s", serverPath, ssePathSuffix)),
internal.WithMessageEndpoint(serverPath)))
conf.servers = append(conf.servers, &SSEServerWrapper{
BaseServer: common.NewSSEServer(serverInstance,
common.WithSSEEndpoint(fmt.Sprintf("%s%s", serverPath, mcp_session.GlobalSSEPathSuffix)),
common.WithMessageEndpoint(serverPath)),
DomainList: serverDomainList,
})
api.LogDebug(fmt.Sprintf("Registered MCP Server: %s", serverType))
}
return conf, nil
}
func (p *parser) Merge(parent interface{}, child interface{}) interface{} {
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 = childConfig.servers
}
if childConfig.defaultServer != nil {
newConfig.defaultServer = childConfig.defaultServer
}
if childConfig.matchList != nil {
newConfig.matchList = childConfig.matchList
}
return &newConfig
}
func filterFactory(c interface{}, callbacks api.FilterCallbackHandler) api.StreamFilter {
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{}),
mcpConfigHandler: handler.NewMCPConfigHandler(conf.redisClient, callbacks),
mcpRatelimitHandler: handler.NewMCPRatelimitHandler(conf.redisClient, callbacks, conf.rateLimitConfig),
config: conf,
callbacks: callbacks,
}
}
func main() {}

View File

@@ -1,104 +1,44 @@
package main
package mcp_server
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/handler"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
"github.com/mark3labs/mcp-go/mcp"
)
const (
RedisNotEnabledResponseBody = "Redis is not enabled, SSE connection is not supported"
)
// 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
userLevelConfig bool
mcpConfigHandler *handler.MCPConfigHandler
ratelimit bool
mcpRatelimitHandler *handler.MCPRatelimitHandler
config *config
req *http.Request
message bool
path string
}
type RequestURL struct {
method string
scheme string
host string
path string
baseURL string
parsedURL *url.URL
internalIP bool
}
func NewRequestURL(header api.RequestHeaderMap) *RequestURL {
method, _ := header.Get(":method")
scheme, _ := header.Get(":scheme")
host, _ := header.Get(":authority")
path, _ := header.Get(":path")
internalIP, _ := header.Get("x-envoy-internal")
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, internalIP: internalIP == "true"}
}
// 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())
url := common.NewRequestURL(header)
if url == nil {
return api.Continue
}
f.path = url.ParsedURL.Path
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())
return api.LocalReply
} else if f.path == server.GetMessageEndpoint() {
if url.method != http.MethodPost {
if common.MatchDomainList(url.ParsedURL.Host, server.DomainList) && url.ParsedURL.Path == server.BaseServer.GetMessageEndpoint() {
if url.Method != http.MethodPost {
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
return api.LocalReply
}
// Create a new http.Request object
f.req = &http.Request{
Method: url.method,
URL: url.parsedURL,
Method: url.Method,
URL: url.ParsedURL,
Header: make(http.Header),
}
api.LogDebugf("Message request: %v", url.parsedURL)
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)
@@ -113,209 +53,33 @@ func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.
}
}
f.req = &http.Request{
Method: url.method,
URL: url.parsedURL,
}
if strings.HasSuffix(f.path, ConfigPathSuffix) && f.config.enableUserLevelServer {
if !url.internalIP {
api.LogWarnf("Access denied: non-internal IP address %s", url.parsedURL.String())
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
return api.LocalReply
}
if strings.HasSuffix(f.path, ConfigPathSuffix) && url.method == http.MethodGet {
api.LogDebugf("Handling config request: %s", f.path)
f.mcpConfigHandler.HandleConfigRequest(f.req, []byte{})
return api.LocalReply
}
f.userLevelConfig = true
if endStream {
return api.Continue
} else {
return api.StopAndBuffer
}
}
if !strings.HasSuffix(url.parsedURL.Path, f.config.ssePathSuffix) {
f.proxyURL = url.parsedURL
if f.config.enableUserLevelServer {
parts := strings.Split(url.parsedURL.Path, "/")
if len(parts) >= 3 {
serverName := parts[1]
uid := parts[2]
// Get encoded config
encodedConfig, _ := f.mcpConfigHandler.GetEncodedConfig(serverName, uid)
if encodedConfig != "" {
header.Set("x-higress-mcpserver-config", encodedConfig)
api.LogDebugf("Set x-higress-mcpserver-config Header for %s:%s", serverName, uid)
}
}
f.ratelimit = true
}
if endStream {
return api.Continue
} else {
return api.StopAndBuffer
}
}
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, "")
}
return api.LocalReply
return api.Continue
}
// 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 !endStream {
return api.StopAndBuffer
}
if f.message {
for _, server := range f.config.servers {
if f.path == server.GetMessageEndpoint() {
if f.path == server.BaseServer.GetMessageEndpoint() {
if !endStream {
return api.StopAndBuffer
}
// Create a response recorder to capture the response
recorder := httptest.NewRecorder()
// Call the handleMessage method of SSEServer with complete body
httpStatus := server.HandleMessage(recorder, f.req, buffer.Bytes())
httpStatus := server.BaseServer.HandleMessage(recorder, f.req, buffer.Bytes())
f.message = false
f.callbacks.DecoderFilterCallbacks().SendLocalReply(httpStatus, recorder.Body.String(), recorder.Header(), 0, "")
return api.LocalReply
}
}
} else if f.userLevelConfig {
// Handle config POST request
api.LogDebugf("Handling config request: %s", f.path)
f.mcpConfigHandler.HandleConfigRequest(f.req, buffer.Bytes())
return api.LocalReply
} else if f.ratelimit {
if checkJSONRPCMethod(buffer.Bytes(), "tools/list") {
api.LogDebugf("Not a tools call request, skipping ratelimit")
return api.Continue
}
parts := strings.Split(f.req.URL.Path, "/")
if len(parts) < 3 {
api.LogWarnf("Access denied: no valid uid found")
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
return api.LocalReply
}
serverName := parts[1]
uid := parts[2]
encodedConfig, err := f.mcpConfigHandler.GetEncodedConfig(serverName, uid)
if err != nil {
api.LogWarnf("Access denied: no valid config found for uid %s", uid)
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
return api.LocalReply
} else if encodedConfig == "" && checkJSONRPCMethod(buffer.Bytes(), "tools/call") {
api.LogDebugf("Empty config found for %s:%s", serverName, uid)
if !f.mcpRatelimitHandler.HandleRatelimit(f.req, buffer.Bytes()) {
return api.LocalReply
}
}
}
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 != "" {
if f.config.redisClient != nil {
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")
} else {
header.Set("Content-Length", strconv.Itoa(len(RedisNotEnabledResponseBody)))
}
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 && f.config.redisClient != 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 != "" {
if f.config.redisClient != nil {
// 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
} else {
buffer.SetString(RedisNotEnabledResponseBody)
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)
}
}
}
// check if the request is a tools/call request
func checkJSONRPCMethod(body []byte, method string) bool {
var request mcp.CallToolRequest
if err := json.Unmarshal(body, &request); err != nil {
api.LogWarnf("Failed to unmarshal request body: %v, not a JSON RPC request", err)
return true
}
return request.Method == method
}

View File

@@ -1,89 +0,0 @@
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

@@ -14,7 +14,7 @@ import (
"github.com/nacos-group/nacos-sdk-go/v2/vo"
)
type NacosMcpRegsitry struct {
type NacosMcpRegistry struct {
serviceMatcher map[string]string
configClient config_client.IConfigClient
namingClient naming_client.INamingClient
@@ -27,7 +27,7 @@ type NacosMcpRegsitry struct {
const DEFAULT_SERVICE_LIST_MAX_PGSIZXE = 10000
const MCP_TOOL_SUBFIX = "-mcp-tools.json"
func (n *NacosMcpRegsitry) ListToolsDesciption() []*registry.ToolDescription {
func (n *NacosMcpRegistry) ListToolsDescription() []*registry.ToolDescription {
if n.toolsDescription == nil {
n.refreshToolsList()
}
@@ -39,7 +39,7 @@ func (n *NacosMcpRegsitry) ListToolsDesciption() []*registry.ToolDescription {
return result
}
func (n *NacosMcpRegsitry) GetToolRpcContext(toolName string) (*registry.RpcContext, bool) {
func (n *NacosMcpRegistry) GetToolRpcContext(toolName string) (*registry.RpcContext, bool) {
if n.toolsRpcContext == nil {
n.refreshToolsList()
}
@@ -47,11 +47,11 @@ func (n *NacosMcpRegsitry) GetToolRpcContext(toolName string) (*registry.RpcCont
return tool, ok
}
func (n *NacosMcpRegsitry) RegisterToolChangeEventListener(listener registry.ToolChangeEventListener) {
func (n *NacosMcpRegistry) RegisterToolChangeEventListener(listener registry.ToolChangeEventListener) {
n.toolChangeEventListeners = append(n.toolChangeEventListeners, listener)
}
func (n *NacosMcpRegsitry) refreshToolsList() bool {
func (n *NacosMcpRegistry) refreshToolsList() bool {
changed := false
for group, serviceMatcher := range n.serviceMatcher {
if n.refreshToolsListForGroup(group, serviceMatcher) {
@@ -61,7 +61,7 @@ func (n *NacosMcpRegsitry) refreshToolsList() bool {
return changed
}
func (n *NacosMcpRegsitry) refreshToolsListForGroup(group string, serviceMatcher string) bool {
func (n *NacosMcpRegistry) refreshToolsListForGroup(group string, serviceMatcher string) bool {
services, err := n.namingClient.GetAllServicesInfo(vo.GetAllServiceInfoParam{
GroupName: group,
PageNo: 1,
@@ -77,7 +77,7 @@ func (n *NacosMcpRegsitry) refreshToolsListForGroup(group string, serviceMatcher
serviceList := services.Doms
pattern, err := regexp.Compile(serviceMatcher)
if err != nil {
api.LogErrorf("Match service error for patter %s", serviceMatcher)
api.LogErrorf("Match service error for pattern %s", serviceMatcher)
return false
}
@@ -134,7 +134,7 @@ func getFormatServiceName(group string, service string) string {
return fmt.Sprintf("%s_%s", group, service)
}
func (n *NacosMcpRegsitry) deleteToolForService(group string, service string) {
func (n *NacosMcpRegistry) deleteToolForService(group string, service string) {
toolsNeedReset := []string{}
formatServiceName := getFormatServiceName(group, service)
@@ -150,7 +150,7 @@ func (n *NacosMcpRegsitry) deleteToolForService(group string, service string) {
}
}
func (n *NacosMcpRegsitry) refreshToolsListForServiceWithContent(group string, service string, newConfig *string, instances *[]model.Instance) bool {
func (n *NacosMcpRegistry) refreshToolsListForServiceWithContent(group string, service string, newConfig *string, instances *[]model.Instance) bool {
if newConfig == nil {
dataId := makeToolsConfigId(service)
@@ -243,7 +243,7 @@ func (n *NacosMcpRegsitry) refreshToolsListForServiceWithContent(group string, s
return true
}
func (n *NacosMcpRegsitry) GetCredential(name string, group string) *registry.CredentialInfo {
func (n *NacosMcpRegistry) GetCredential(name string, group string) *registry.CredentialInfo {
dataId := makeCredentialDataId(name)
content, err := n.configClient.GetConfig(vo.ConfigParam{
DataId: dataId,
@@ -265,11 +265,11 @@ func (n *NacosMcpRegsitry) GetCredential(name string, group string) *registry.Cr
return &credential
}
func (n *NacosMcpRegsitry) refreshToolsListForService(group string, service string) bool {
func (n *NacosMcpRegistry) refreshToolsListForService(group string, service string) bool {
return n.refreshToolsListForServiceWithContent(group, service, nil, nil)
}
func (n *NacosMcpRegsitry) listenToService(group string, service string) {
func (n *NacosMcpRegistry) listenToService(group string, service string) {
// config changed, tools description may be changed
err := n.configClient.ListenConfig(vo.ConfigParam{

View File

@@ -5,8 +5,8 @@ import (
"fmt"
"time"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/registry"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
"github.com/mark3labs/mcp-go/mcp"
"github.com/nacos-group/nacos-sdk-go/v2/clients"
@@ -15,7 +15,7 @@ import (
)
func init() {
internal.GlobalRegistry.RegisterServer("nacos-mcp-registry", &NacosConfig{})
common.GlobalRegistry.RegisterServer("nacos-mcp-registry", &NacosConfig{})
}
type NacosConfig struct {
@@ -28,14 +28,14 @@ type NacosConfig struct {
}
type McpServerToolsChangeListener struct {
mcpServer *internal.MCPServer
mcpServer *common.MCPServer
}
func (l *McpServerToolsChangeListener) OnToolChanged(reg registry.McpServerRegistry) {
resetToolsToMcpServer(l.mcpServer, reg)
}
func CreateNacosMcpRegsitry(config *NacosConfig) (*NacosMcpRegsitry, error) {
func CreateNacosMcpRegistry(config *NacosConfig) (*NacosMcpRegistry, error) {
sc := []constant.ServerConfig{
*constant.NewServerConfig(*config.ServerAddr, 8848, constant.WithContextPath("/nacos")),
}
@@ -90,7 +90,7 @@ func CreateNacosMcpRegsitry(config *NacosConfig) (*NacosMcpRegsitry, error) {
return nil, fmt.Errorf("failed to initial naming config client: %w", err)
}
return &NacosMcpRegsitry{
return &NacosMcpRegistry{
configClient: configClient,
namingClient: namingClient,
serviceMatcher: *config.ServiceMatcher,
@@ -137,13 +137,13 @@ func (c *NacosConfig) ParseConfig(config map[string]any) error {
return nil
}
func (c *NacosConfig) NewServer(serverName string) (*internal.MCPServer, error) {
mcpServer := internal.NewMCPServer(
func (c *NacosConfig) NewServer(serverName string) (*common.MCPServer, error) {
mcpServer := common.NewMCPServer(
serverName,
"1.0.0",
)
nacosRegistry, err := CreateNacosMcpRegsitry(c)
nacosRegistry, err := CreateNacosMcpRegistry(c)
if err != nil {
return nil, fmt.Errorf("failed to initialize NacosMcpRegistry: %w", err)
}
@@ -170,11 +170,11 @@ func (c *NacosConfig) NewServer(serverName string) (*internal.MCPServer, error)
return mcpServer, nil
}
func resetToolsToMcpServer(mcpServer *internal.MCPServer, reg registry.McpServerRegistry) {
wrappedTools := []internal.ServerTool{}
tools := reg.ListToolsDesciption()
func resetToolsToMcpServer(mcpServer *common.MCPServer, reg registry.McpServerRegistry) {
wrappedTools := []common.ServerTool{}
tools := reg.ListToolsDescription()
for _, tool := range tools {
wrappedTools = append(wrappedTools, internal.ServerTool{
wrappedTools = append(wrappedTools, common.ServerTool{
Tool: mcp.NewToolWithRawSchema(tool.Name, tool.Description, tool.InputSchema),
Handler: registry.HandleRegistryToolsCall(reg),
})

View File

@@ -36,7 +36,7 @@ type ToolChangeEventListener interface {
}
type McpServerRegistry interface {
ListToolsDesciption() []*ToolDescription
ListToolsDescription() []*ToolDescription
GetToolRpcContext(toolname string) (*RpcContext, bool)
RegisterToolChangeEventListener(listener ToolChangeEventListener)
}

View File

@@ -9,7 +9,7 @@ import (
"net/url"
"strings"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
"github.com/mark3labs/mcp-go/mcp"
)
@@ -177,7 +177,7 @@ func selectOneInstance(ctx *RpcContext) (*Instance, error) {
return &select_instance, nil
}
func getRemoteCallhandle(ctx *RpcContext) (RemoteCallHandle, error) {
func getRemoteCallHandle(ctx *RpcContext) (RemoteCallHandle, error) {
if ctx.Protocol == PROTOCOL_HTTP || ctx.Protocol == PROTOCOL_HTTPS {
return newHttpRemoteCallHandle(ctx)
} else {
@@ -192,7 +192,7 @@ func CommonRemoteCall(reg McpServerRegistry, toolName string, parameters map[str
return nil, fmt.Errorf("Unknown tool %s", toolName)
}
remoteHandle, err := getRemoteCallhandle(ctx)
remoteHandle, err := getRemoteCallHandle(ctx)
if remoteHandle == nil {
return nil, fmt.Errorf("Unknown backend protocol %s", ctx.Protocol)
}
@@ -204,7 +204,7 @@ func CommonRemoteCall(reg McpServerRegistry, toolName string, parameters map[str
return remoteHandle.HandleToolCall(ctx, parameters)
}
func HandleRegistryToolsCall(reg McpServerRegistry) internal.ToolHandlerFunc {
func HandleRegistryToolsCall(reg McpServerRegistry) common.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

@@ -3,6 +3,7 @@ package gorm
import (
"context"
"fmt"
"strings"
"sync/atomic"
"time"
@@ -25,6 +26,14 @@ type DBClient struct {
panicCount int32 // Add panic counter
}
// supports database types
const (
MYSQL = "mysql"
POSTGRES = "postgres"
CLICKHOUSE = "clickhouse"
SQLITE = "sqlite"
)
// NewDBClient creates a new DBClient instance and establishes a connection to the database
func NewDBClient(dsn string, dbType string, stop chan struct{}) *DBClient {
client := &DBClient{
@@ -53,13 +62,13 @@ func (c *DBClient) connect() error {
}
switch c.dbType {
case "postgres":
case POSTGRES:
db, err = gorm.Open(postgres.Open(c.dsn), &gormConfig)
case "clickhouse":
case CLICKHOUSE:
db, err = gorm.Open(clickhouse.Open(c.dsn), &gormConfig)
case "mysql":
case MYSQL:
db, err = gorm.Open(mysql.Open(c.dsn), &gormConfig)
case "sqlite":
case SQLITE:
db, err = gorm.Open(sqlite.Open(c.dsn), &gormConfig)
default:
return fmt.Errorf("unsupported database type %s", c.dbType)
@@ -125,25 +134,164 @@ func (c *DBClient) reconnectLoop() {
}
}
// 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) {
func (c *DBClient) reconnectIfDbEmpty() error {
if c.db == nil {
// Trigger reconnection
select {
case c.reconnect <- struct{}{}:
default:
}
return nil, fmt.Errorf("database is not connected, attempting to reconnect")
return fmt.Errorf("database is not connected, attempting to reconnect")
}
return nil
}
rows, err := c.db.Raw(query, args...).Rows()
func (c *DBClient) handleSQLError(err error) error {
if err != nil {
// If execution fails, connection might be lost, trigger reconnection
select {
case c.reconnect <- struct{}{}:
default:
}
return nil, fmt.Errorf("failed to execute SQL query: %w", err)
return fmt.Errorf("failed to execute SQL: %w", err)
}
return nil
}
// DescribeTable Get the structure of a specific table.
func (c *DBClient) DescribeTable(table string) ([]map[string]interface{}, error) {
var sql string
switch c.dbType {
case MYSQL:
sql = `
select
column_name,
column_type,
is_nullable,
column_key,
column_default,
extra,
column_comment
from information_schema.columns
where table_schema = database() and table_name = ?
`
return c.Query(sql, table)
case POSTGRES:
sql = `
select
column_name,
data_type as column_type,
is_nullable,
case
when column_default like 'nextval%' then 'auto_increment'
when column_default is not null then 'default'
else ''
end as column_key,
column_default,
case
when column_default like 'nextval%' then 'auto_increment'
else ''
end as extra,
col_description((select oid from pg_class where relname = ?), ordinal_position) as column_comment
from information_schema.columns
where table_name = ?
`
lowerTable := strings.ToLower(table)
return c.Query(sql, lowerTable, lowerTable)
case CLICKHOUSE:
sql = `
select
name as column_name,
type as column_type,
if(is_nullable, 'YES', 'NO') as is_nullable,
default_kind as column_key,
default_expression as column_default,
default_kind as extra,
comment as column_comment
from system.columns
where database = currentDatabase() and table = ?
`
return c.Query(sql, table)
case SQLITE:
sql = `
select
name as column_name,
type as column_type,
not (notnull = 1) as is_nullable,
pk as column_key,
dflt_value as column_default,
'' as extra,
'' as column_comment
from pragma_table_info(?)
`
return c.Query(sql, table)
default:
return nil, fmt.Errorf("unsupported database type: %s", c.dbType)
}
}
// ListTables List all tables in the connected database.
func (c *DBClient) ListTables() ([]string, error) {
var sql string
switch c.dbType {
case MYSQL:
sql = "show tables"
case POSTGRES:
sql = "select tablename from pg_tables where schemaname = 'public'"
case CLICKHOUSE:
sql = "select name from system.tables where database = currentDatabase()"
case SQLITE:
sql = "select name from sqlite_master where type='table'"
default:
return nil, fmt.Errorf("unsupported database type: %s", c.dbType)
}
rows, err := c.db.Raw(sql).Rows()
if err := c.handleSQLError(err); err != nil {
return nil, err
}
defer rows.Close()
var tables []string
for rows.Next() {
var table string
if err := rows.Scan(&table); err != nil {
return nil, fmt.Errorf("failed to scan table name: %w", err)
}
tables = append(tables, table)
}
return tables, nil
}
// Execute executes an INSERT, UPDATE, or DELETE raw SQL and returns the rows affected
func (c *DBClient) Execute(sql string, args ...interface{}) (int64, error) {
if err := c.reconnectIfDbEmpty(); err != nil {
return 0, err
}
tx := c.db.Exec(sql, args...)
if err := c.handleSQLError(tx.Error); err != nil {
return 0, err
}
defer tx.Commit()
return tx.RowsAffected, nil
}
// Query executes a raw SQL query and returns the result as a slice of maps
func (c *DBClient) Query(sql string, args ...interface{}) ([]map[string]interface{}, error) {
if err := c.reconnectIfDbEmpty(); err != nil {
return nil, err
}
rows, err := c.db.Raw(sql, args...).Rows()
if err := c.handleSQLError(err); err != nil {
return nil, err
}
defer rows.Close()

View File

@@ -4,7 +4,7 @@ import (
"errors"
"fmt"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
"github.com/mark3labs/mcp-go/mcp"
)
@@ -12,7 +12,7 @@ import (
const Version = "1.0.0"
func init() {
internal.GlobalRegistry.RegisterServer("database", &DBConfig{})
common.GlobalRegistry.RegisterServer("database", &DBConfig{})
}
type DBConfig struct {
@@ -41,19 +41,32 @@ func (c *DBConfig) ParseConfig(config map[string]any) error {
return nil
}
func (c *DBConfig) NewServer(serverName string) (*internal.MCPServer, error) {
mcpServer := internal.NewMCPServer(
func (c *DBConfig) NewServer(serverName string) (*common.MCPServer, error) {
mcpServer := common.NewMCPServer(
serverName,
Version,
internal.WithInstructions(fmt.Sprintf("This is a %s database server", c.dbType)),
common.WithInstructions(fmt.Sprintf("This is a %s database server", c.dbType)),
)
dbClient := NewDBClient(c.dsn, c.dbType, mcpServer.GetDestoryChannel())
descriptionSuffix := fmt.Sprintf("in database %s. Database description: %s", c.dbType, c.description)
// Add query tool
mcpServer.AddTool(
mcp.NewToolWithRawSchema("query", fmt.Sprintf("Run a read-only SQL query in database %s. Database description: %s", c.dbType, c.description), GetQueryToolSchema()),
mcp.NewToolWithRawSchema("query", fmt.Sprintf("Run a read-only SQL query %s", descriptionSuffix), GetQueryToolSchema()),
HandleQueryTool(dbClient),
)
mcpServer.AddTool(
mcp.NewToolWithRawSchema("execute", fmt.Sprintf("Execute an insert, update, or delete SQL %s", descriptionSuffix), GetExecuteToolSchema()),
HandleExecuteTool(dbClient),
)
mcpServer.AddTool(
mcp.NewToolWithRawSchema("list tables", fmt.Sprintf("List all tables %s", descriptionSuffix), GetListTablesToolSchema()),
HandleListTablesTool(dbClient),
)
mcpServer.AddTool(
mcp.NewToolWithRawSchema("describe table", fmt.Sprintf("Get the structure of a specific table %s", descriptionSuffix), GetDescribeTableToolSchema()),
HandleDescribeTableTool(dbClient),
)
return mcpServer, nil
}

View File

@@ -5,12 +5,12 @@ import (
"encoding/json"
"fmt"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
"github.com/mark3labs/mcp-go/mcp"
)
// HandleQueryTool handles SQL query execution
func HandleQueryTool(dbClient *DBClient) internal.ToolHandlerFunc {
func HandleQueryTool(dbClient *DBClient) common.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
arguments := request.Params.Arguments
message, ok := arguments["sql"].(string)
@@ -18,27 +18,80 @@ func HandleQueryTool(dbClient *DBClient) internal.ToolHandlerFunc {
return nil, fmt.Errorf("invalid message argument")
}
results, err := dbClient.ExecuteSQL(message)
results, err := dbClient.Query(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 buildCallToolResult(results)
}
}
// HandleExecuteTool handles SQL INSERT, UPDATE, or DELETE execution
func HandleExecuteTool(dbClient *DBClient) common.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")
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(jsonData),
},
},
}, nil
results, err := dbClient.Execute(message)
if err != nil {
return nil, fmt.Errorf("failed to execute SQL query: %w", err)
}
return buildCallToolResult(results)
}
}
// HandleListTablesTool handles list all tables
func HandleListTablesTool(dbClient *DBClient) common.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
results, err := dbClient.ListTables()
if err != nil {
return nil, fmt.Errorf("failed to execute SQL query: %w", err)
}
return buildCallToolResult(results)
}
}
// HandleDescribeTableTool handles describe table
func HandleDescribeTableTool(dbClient *DBClient) common.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
arguments := request.Params.Arguments
message, ok := arguments["table"].(string)
if !ok {
return nil, fmt.Errorf("invalid message argument")
}
results, err := dbClient.DescribeTable(message)
if err != nil {
return nil, fmt.Errorf("failed to execute SQL query: %w", err)
}
return buildCallToolResult(results)
}
}
// buildCallToolResult builds the call tool result
func buildCallToolResult(results any) (*mcp.CallToolResult, error) {
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(`
@@ -53,3 +106,44 @@ func GetQueryToolSchema() json.RawMessage {
}
`)
}
// GetExecuteToolSchema returns the schema for execute tool
func GetExecuteToolSchema() json.RawMessage {
return json.RawMessage(`
{
"type": "object",
"properties": {
"sql": {
"type": "string",
"description": "The sql to execute"
}
}
}
`)
}
// GetDescribeTableToolSchema returns the schema for DescribeTable tool
func GetDescribeTableToolSchema() json.RawMessage {
return json.RawMessage(`
{
"type": "object",
"properties": {
"table": {
"type": "string",
"description": "table name"
}
}
}
`)
}
// GetListTablesToolSchema returns the schema for ListTables tool
func GetListTablesToolSchema() json.RawMessage {
return json.RawMessage(`
{
"type": "object",
"properties": {
}
}
`)
}

View File

@@ -0,0 +1,95 @@
package higress
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
)
// HigressClient handles Higress Console API connections and operations
type HigressClient struct {
baseURL string
username string
password string
httpClient *http.Client
}
func NewHigressClient(baseURL, username, password string) *HigressClient {
client := &HigressClient{
baseURL: baseURL,
username: username,
password: password,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
api.LogInfof("Higress Console client initialized: %s", baseURL)
return client
}
func (c *HigressClient) Get(path string) ([]byte, error) {
return c.request("GET", path, nil)
}
func (c *HigressClient) Post(path string, data interface{}) ([]byte, error) {
return c.request("POST", path, data)
}
func (c *HigressClient) Put(path string, data interface{}) ([]byte, error) {
return c.request("PUT", path, data)
}
func (c *HigressClient) Delete(path string) ([]byte, error) {
return c.request("DELETE", path, nil)
}
func (c *HigressClient) request(method, path string, data interface{}) ([]byte, error) {
url := c.baseURL + path
var body io.Reader
if data != nil {
jsonData, err := json.Marshal(data)
if err != nil {
return nil, fmt.Errorf("failed to marshal request data: %w", err)
}
body = bytes.NewBuffer(jsonData)
api.LogDebugf("Higress API %s %s: %s", method, url, string(jsonData))
} else {
api.LogDebugf("Higress API %s %s", method, url)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.SetBasicAuth(c.username, c.password)
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("HTTP error %d", resp.StatusCode)
}
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
return respBody, nil
}

View File

@@ -0,0 +1,73 @@
# Higress API MCP Server
Higress API MCP Server 提供了 MCP 工具来管理 Higress 路由、服务来源和插件等资源。
## 功能特性
### 路由管理
- `list-routes`: 列出路由
- `get-route`: 获取路由
- `add-route`: 添加路由
- `update-route`: 更新路由
### 服务来源管理
- `list-service-sources`: 列出服务来源
- `get-service-source`: 获取服务来源
- `add-service-source`: 添加服务来源
- `update-service-source`: 更新服务来源
### 插件管理
- `get-plugin`: 获取插件配置
- `delete-plugin`: 删除插件
- `update-request-block-plugin`: 更新 request-block 插件配置
## 配置参数
| 参数 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `higressURL` | string | 必填 | Higress Console 的 URL 地址 |
| `username` | string | 必填 | Higress Console 登录用户名 |
| `password` | string | 必填 | Higress Console 登录密码 |
| `description` | string | 可选 | 服务器描述信息 |
配置示例:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
annotations:
meta.helm.sh/release-name: higress
meta.helm.sh/release-namespace: higress-system
labels:
app: higress-gateway
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: higress-gateway
app.kubernetes.io/version: 2.1.4
helm.sh/chart: higress-core-2.1.4
higress: higress-system-higress-gateway
name: higress-config
namespace: higress-system
data:
higress: |-
mcpServer:
sse_path_suffix: /sse # SSE 连接的路径后缀
enable: true # 启用 MCP Server
redis:
address: redis-stack-server.higress-system.svc.cluster.local:6379 # Redis服务地址
username: "" # Redis用户名可选
password: "" # Redis密码可选
db: 0 # Redis数据库可选
match_list: # MCP Server 会话保持路由规则(当匹配下面路径时,将被识别为一个 MCP 会话,通过 SSE 等机制进行会话保持)
- match_rule_domain: "*"
match_rule_path: /higress-api
match_rule_type: "prefix"
servers:
- name: higress-api-mcp-server # MCP Server 名称
path: /higress-api # 访问路径,需要与 match_list 中的配置匹配
type: higress-api # 类型和 RegisterServer 一致
config:
higressURL: http://higress-console.higress-system.svc.cluster.local:8080
username: admin
password: admin
```

View File

@@ -0,0 +1,73 @@
# Higress API MCP Server
Higress API MCP Server provides MCP tools to manage Higress routes, service sources, plugins and other resources.
## Features
### Route Management
- `list-routes`: List routes
- `get-route`: Get route
- `add-route`: Add route
- `update-route`: Update route
### Service Source Management
- `list-service-sources`: List service sources
- `get-service-source`: Get service source
- `add-service-source`: Add service source
- `update-service-source`: Update service source
### Plugin Management
- `get-plugin`: Get plugin configuration
- `delete-plugin`: Delete plugin
- `update-request-block-plugin`: Update request block configuration
## Configuration Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `higressURL` | string | Required | Higress Console URL address |
| `username` | string | Required | Higress Console login username |
| `password` | string | Required | Higress Console login password |
| `description` | string | Optional | MCP Server description |
Configuration Example:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
annotations:
meta.helm.sh/release-name: higress
meta.helm.sh/release-namespace: higress-system
labels:
app: higress-gateway
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: higress-gateway
app.kubernetes.io/version: 2.1.4
helm.sh/chart: higress-core-2.1.4
higress: higress-system-higress-gateway
name: higress-config
namespace: higress-system
data:
higress: |-
mcpServer:
sse_path_suffix: /sse # SSE connection path suffix
enable: true # Enable MCP Server
redis:
address: redis-stack-server.higress-system.svc.cluster.local:6379 # Redis service address
username: "" # Redis username (optional)
password: "" # Redis password (optional)
db: 0 # Redis database (optional)
match_list: # MCP Server session persistence routing rules (when matching the following paths, it will be recognized as an MCP session and maintained through SSE)
- match_rule_domain: "*"
match_rule_path: /higress-api
match_rule_type: "prefix"
servers:
- name: higress-api-mcp-server # MCP Server name
path: /higress-api # Access path, needs to match the configuration in match_list
type: higress-api # Type defined in RegisterServer function
config:
higressURL: http://higress-console.higress-system.svc.cluster.local:8080
username: admin
password: admin
```

View File

@@ -0,0 +1,76 @@
package higress_ops
import (
"errors"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/plugins"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
)
const Version = "1.0.0"
func init() {
common.GlobalRegistry.RegisterServer("higress-api", &HigressConfig{})
}
type HigressConfig struct {
higressURL string
username string
password string
description string
}
func (c *HigressConfig) ParseConfig(config map[string]interface{}) error {
higressURL, ok := config["higressURL"].(string)
if !ok {
return errors.New("missing higressURL")
}
c.higressURL = higressURL
username, ok := config["username"].(string)
if !ok {
return errors.New("missing username")
}
c.username = username
password, ok := config["password"].(string)
if !ok {
return errors.New("missing password")
}
c.password = password
if desc, ok := config["description"].(string); ok {
c.description = desc
} else {
c.description = "Higress API MCP Server, which invokes Higress Console APIs to manage resources such as routes, services, and plugins."
}
api.LogDebugf("HigressConfig ParseConfig: higressURL=%s, username=%s, description=%s",
c.higressURL, c.username, c.description)
return nil
}
func (c *HigressConfig) NewServer(serverName string) (*common.MCPServer, error) {
mcpServer := common.NewMCPServer(
serverName,
Version,
common.WithInstructions("This is a Higress API MCP Server"),
)
// Initialize Higress API client
client := higress.NewHigressClient(c.higressURL, c.username, c.password)
// Register all tools
tools.RegisterRouteTools(mcpServer, client)
tools.RegisterServiceTools(mcpServer, client)
plugins.RegisterCommonPluginTools(mcpServer, client)
plugins.RegisterRequestBlockPluginTools(mcpServer, client)
api.LogInfof("Higress MCP Server initialized: %s", serverName)
return mcpServer, nil
}

View File

@@ -0,0 +1,141 @@
package plugins
import (
"context"
"encoding/json"
"fmt"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
"github.com/mark3labs/mcp-go/mcp"
)
// RegisterCommonPluginTools registers all common plugin management tools
func RegisterCommonPluginTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
// Get plugin configuration
mcpServer.AddTool(
mcp.NewToolWithRawSchema("get-plugin", "Get configuration for a specific plugin", getPluginConfigSchema()),
handleGetPluginConfig(client),
)
// Delete plugin configuration
mcpServer.AddTool(
mcp.NewToolWithRawSchema("delete-plugin", "Delete configuration for a specific plugin", getPluginConfigSchema()),
handleDeletePluginConfig(client),
)
}
func handleGetPluginConfig(client *higress.HigressClient) common.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
arguments := request.Params.Arguments
// Parse required parameters
pluginName, ok := arguments["name"].(string)
if !ok {
return nil, fmt.Errorf("missing or invalid 'name' argument")
}
scope, ok := arguments["scope"].(string)
if !ok {
return nil, fmt.Errorf("missing or invalid 'scope' argument")
}
if !IsValidScope(scope) {
return nil, fmt.Errorf("invalid scope '%s', must be one of: %v", scope, ValidScopes)
}
// Parse resource_name (required for non-global scopes)
var resourceName string
if scope != ScopeGlobal {
resourceName, ok = arguments["resource_name"].(string)
if !ok || resourceName == "" {
return nil, fmt.Errorf("'resource_name' is required for scope '%s'", scope)
}
}
// Build API path and make request
path := BuildPluginPath(pluginName, scope, resourceName)
respBody, err := client.Get(path)
if err != nil {
return nil, fmt.Errorf("failed to get plugin config for '%s' at scope '%s': %w", pluginName, scope, err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(respBody),
},
},
}, nil
}
}
func handleDeletePluginConfig(client *higress.HigressClient) common.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
arguments := request.Params.Arguments
// Parse required parameters
pluginName, ok := arguments["name"].(string)
if !ok {
return nil, fmt.Errorf("missing or invalid 'name' argument")
}
scope, ok := arguments["scope"].(string)
if !ok {
return nil, fmt.Errorf("missing or invalid 'scope' argument")
}
if !IsValidScope(scope) {
return nil, fmt.Errorf("invalid scope '%s', must be one of: %v", scope, ValidScopes)
}
// Parse resource_name (required for non-global scopes)
var resourceName string
if scope != ScopeGlobal {
resourceName, ok = arguments["resource_name"].(string)
if !ok || resourceName == "" {
return nil, fmt.Errorf("'resource_name' is required for scope '%s'", scope)
}
}
// Build API path and make request
path := BuildPluginPath(pluginName, scope, resourceName)
respBody, err := client.Delete(path)
if err != nil {
return nil, fmt.Errorf("failed to delete plugin config for '%s' at scope '%s': %w", pluginName, scope, err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(respBody),
},
},
}, nil
}
}
func getPluginConfigSchema() json.RawMessage {
return json.RawMessage(`{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the plugin"
},
"scope": {
"type": "string",
"enum": ["GLOBAL", "DOMAIN", "SERVICE", "ROUTE"],
"description": "The scope at which the plugin is applied"
},
"resource_name": {
"type": "string",
"description": "The name of the resource (required for DOMAIN, SERVICE, ROUTE scopes)"
}
},
"required": ["name", "scope"],
"additionalProperties": false
}`)
}

View File

@@ -0,0 +1,186 @@
package plugins
import (
"context"
"encoding/json"
"fmt"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
"github.com/mark3labs/mcp-go/mcp"
)
const RequestBlockPluginName = "request-block"
// RequestBlockConfig represents the configuration for request-block plugin
type RequestBlockConfig struct {
BlockBodies []string `json:"block_bodies,omitempty"`
BlockHeaders []string `json:"block_headers,omitempty"`
BlockUrls []string `json:"block_urls,omitempty"`
BlockedCode int `json:"blocked_code,omitempty"`
CaseSensitive bool `json:"case_sensitive,omitempty"`
}
// RequestBlockInstance represents a request-block plugin instance
type RequestBlockInstance = PluginInstance[RequestBlockConfig]
// RequestBlockResponse represents the API response for request-block plugin
type RequestBlockResponse = higress.APIResponse[RequestBlockInstance]
// RegisterRequestBlockPluginTools registers all request block plugin management tools
func RegisterRequestBlockPluginTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
// Update request block configuration
mcpServer.AddTool(
mcp.NewToolWithRawSchema(fmt.Sprintf("update-%s-plugin", RequestBlockPluginName), "Update request block plugin configuration", getAddOrUpdateRequestBlockConfigSchema()),
handleAddOrUpdateRequestBlockConfig(client),
)
}
func handleAddOrUpdateRequestBlockConfig(client *higress.HigressClient) common.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
arguments := request.Params.Arguments
// Parse required parameters
scope, ok := arguments["scope"].(string)
if !ok {
return nil, fmt.Errorf("missing or invalid 'scope' argument")
}
if !IsValidScope(scope) {
return nil, fmt.Errorf("invalid scope '%s', must be one of: %v", scope, ValidScopes)
}
enabled, ok := arguments["enabled"].(bool)
if !ok {
return nil, fmt.Errorf("missing or invalid 'enabled' argument")
}
configurations, ok := arguments["configurations"]
if !ok {
return nil, fmt.Errorf("missing 'configurations' argument")
}
// Parse resource_name for non-global scopes
var resourceName string
if scope != ScopeGlobal {
// Validate and get resource_name
resourceName, ok = arguments["resource_name"].(string)
if !ok || resourceName == "" {
return nil, fmt.Errorf("'resource_name' is required for scope '%s'", scope)
}
}
// Build API path
path := BuildPluginPath(RequestBlockPluginName, scope, resourceName)
// Get current request block configuration to merge with updates
currentBody, err := client.Get(path)
if err != nil {
return nil, fmt.Errorf("failed to get current request block configuration: %w", err)
}
var response RequestBlockResponse
if err := json.Unmarshal(currentBody, &response); err != nil {
return nil, fmt.Errorf("failed to parse current request block response: %w", err)
}
currentConfig := response.Data
currentConfig.Enabled = enabled
currentConfig.Scope = scope
// Convert the input configurations to RequestBlockConfig and merge
configBytes, err := json.Marshal(configurations)
if err != nil {
return nil, fmt.Errorf("failed to marshal configurations: %w", err)
}
var newConfig RequestBlockConfig
if err := json.Unmarshal(configBytes, &newConfig); err != nil {
return nil, fmt.Errorf("failed to parse request block configurations: %w", err)
}
// Update configurations (overwrite with new values where provided)
if newConfig.BlockBodies != nil {
currentConfig.Configurations.BlockBodies = newConfig.BlockBodies
}
if newConfig.BlockHeaders != nil {
currentConfig.Configurations.BlockHeaders = newConfig.BlockHeaders
}
if newConfig.BlockUrls != nil {
currentConfig.Configurations.BlockUrls = newConfig.BlockUrls
}
if newConfig.BlockedCode != 0 {
currentConfig.Configurations.BlockedCode = newConfig.BlockedCode
}
currentConfig.Configurations.CaseSensitive = newConfig.CaseSensitive
respBody, err := client.Put(path, currentConfig)
if err != nil {
return nil, fmt.Errorf("failed to update request block config at scope '%s': %w", scope, err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(respBody),
},
},
}, nil
}
}
func getAddOrUpdateRequestBlockConfigSchema() json.RawMessage {
return json.RawMessage(`{
"type": "object",
"properties": {
"scope": {
"type": "string",
"enum": ["GLOBAL", "DOMAIN", "SERVICE", "ROUTE"],
"description": "The scope at which the plugin is applied"
},
"resource_name": {
"type": "string",
"description": "The name of the resource (required for DOMAIN, SERVICE, ROUTE scopes)"
},
"enabled": {
"type": "boolean",
"description": "Whether the plugin is enabled"
},
"configurations": {
"type": "object",
"properties": {
"block_bodies": {
"type": "array",
"items": {"type": "string"},
"description": "List of patterns to match against request body content"
},
"block_headers": {
"type": "array",
"items": {"type": "string"},
"description": "List of patterns to match against request headers"
},
"block_urls": {
"type": "array",
"items": {"type": "string"},
"description": "List of patterns to match against request URLs"
},
"blocked_code": {
"type": "integer",
"minimum": 100,
"maximum": 599,
"description": "HTTP status code to return when a block is matched"
},
"case_sensitive": {
"type": "boolean",
"description": "Whether the block matching is case sensitive"
}
},
"additionalProperties": false
}
},
"required": ["scope", "enabled", "configurations"],
"additionalProperties": false
}`)
}

View File

@@ -0,0 +1,22 @@
package plugins
// PluginTargets represents the targets for different scopes
type PluginTargets struct {
Domain string `json:"DOMAIN,omitempty"`
Service string `json:"SERVICE,omitempty"`
Route string `json:"ROUTE,omitempty"`
}
// PluginInstance represents a plugin instance configuration
type PluginInstance[T any] struct {
Version string `json:"version,omitempty"`
Scope string `json:"scope"`
Target string `json:"target,omitempty"`
Targets PluginTargets `json:"targets,omitempty"`
PluginName string `json:"pluginName,omitempty"`
PluginVersion string `json:"pluginVersion,omitempty"`
Internal bool `json:"internal,omitempty"`
Enabled bool `json:"enabled"`
RawConfigurations string `json:"rawConfigurations,omitempty"`
Configurations T `json:"configurations,omitempty"`
}

View File

@@ -0,0 +1,39 @@
package plugins
import "fmt"
const (
ScopeGlobal = "GLOBAL"
ScopeDomain = "DOMAIN"
ScopeService = "SERVICE"
ScopeRoute = "ROUTE"
)
// ValidScopes contains all valid plugin scopes
var ValidScopes = []string{ScopeGlobal, ScopeDomain, ScopeService, ScopeRoute}
// IsValidScope checks if the given scope is valid
func IsValidScope(scope string) bool {
for _, validScope := range ValidScopes {
if scope == validScope {
return true
}
}
return false
}
// BuildPluginPath builds the API path for plugin operations based on scope and resource
func BuildPluginPath(pluginName, scope, resourceName string) string {
switch scope {
case ScopeGlobal:
return fmt.Sprintf("/v1/global/plugin-instances/%s", pluginName)
case ScopeDomain:
return fmt.Sprintf("/v1/domains/%s/plugin-instances/%s", resourceName, pluginName)
case ScopeService:
return fmt.Sprintf("/v1/services/%s/plugin-instances/%s", resourceName, pluginName)
case ScopeRoute:
return fmt.Sprintf("/v1/routes/%s/plugin-instances/%s", resourceName, pluginName)
default:
return fmt.Sprintf("/v1/global/plugin-instances/%s", pluginName)
}
}

View File

@@ -0,0 +1,456 @@
package tools
import (
"context"
"encoding/json"
"fmt"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
"github.com/mark3labs/mcp-go/mcp"
)
// Route represents a route configuration
type Route struct {
Name string `json:"name"`
Version string `json:"version,omitempty"`
Domains []string `json:"domains,omitempty"`
Path *RoutePath `json:"path,omitempty"`
Methods []string `json:"methods,omitempty"`
Headers []RouteMatch `json:"headers,omitempty"`
URLParams []RouteMatch `json:"urlParams,omitempty"`
Services []RouteService `json:"services,omitempty"`
AuthConfig *RouteAuthConfig `json:"authConfig,omitempty"`
CustomConfigs map[string]interface{} `json:"customConfigs,omitempty"`
}
// RoutePath represents path matching configuration
type RoutePath struct {
MatchType string `json:"matchType"`
MatchValue string `json:"matchValue"`
CaseSensitive bool `json:"caseSensitive,omitempty"`
}
// RouteMatch represents header or URL parameter matching configuration
type RouteMatch struct {
Key string `json:"key"`
MatchType string `json:"matchType"`
MatchValue string `json:"matchValue"`
}
// RouteService represents a service in the route
type RouteService struct {
Name string `json:"name"`
Port int `json:"port"`
Weight int `json:"weight"`
}
// RouteAuthConfig represents authentication configuration for a route
type RouteAuthConfig struct {
Enabled bool `json:"enabled"`
AllowedConsumers []string `json:"allowedConsumers,omitempty"`
}
// RouteResponse represents the API response for route operations
type RouteResponse = higress.APIResponse[Route]
// RegisterRouteTools registers all route management tools
func RegisterRouteTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
// List all routes
mcpServer.AddTool(
mcp.NewTool("list-routes", mcp.WithDescription("List all available routes")),
handleListRoutes(client),
)
// Get specific route
mcpServer.AddTool(
mcp.NewToolWithRawSchema("get-route", "Get detailed information about a specific route", getRouteSchema()),
handleGetRoute(client),
)
// Add new route
mcpServer.AddTool(
mcp.NewToolWithRawSchema("add-route", "Add a new route", getAddRouteSchema()),
handleAddRoute(client),
)
// Update existing route
mcpServer.AddTool(
mcp.NewToolWithRawSchema("update-route", "Update an existing route", getUpdateRouteSchema()),
handleUpdateRoute(client),
)
// Delete existing route
mcpServer.AddTool(
mcp.NewToolWithRawSchema("delete-route", "Delete an existing route", getRouteSchema()),
handleDeleteRoute(client),
)
}
func handleListRoutes(client *higress.HigressClient) common.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
respBody, err := client.Get("/v1/routes")
if err != nil {
return nil, fmt.Errorf("failed to list routes: %w", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(respBody),
},
},
}, nil
}
}
func handleGetRoute(client *higress.HigressClient) common.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
arguments := request.Params.Arguments
name, ok := arguments["name"].(string)
if !ok {
return nil, fmt.Errorf("missing or invalid 'name' argument")
}
respBody, err := client.Get(fmt.Sprintf("/v1/routes/%s", name))
if err != nil {
return nil, fmt.Errorf("failed to get route '%s': %w", name, err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(respBody),
},
},
}, nil
}
}
func handleAddRoute(client *higress.HigressClient) common.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
arguments := request.Params.Arguments
configurations, ok := arguments["configurations"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("missing or invalid 'configurations' argument")
}
// Validate required fields
if _, ok := configurations["name"]; !ok {
return nil, fmt.Errorf("missing required field 'name' in configurations")
}
if _, ok := configurations["path"]; !ok {
return nil, fmt.Errorf("missing required field 'path' in configurations")
}
if _, ok := configurations["services"]; !ok {
return nil, fmt.Errorf("missing required field 'services' in configurations")
}
respBody, err := client.Post("/v1/routes", configurations)
if err != nil {
return nil, fmt.Errorf("failed to add route: %w", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(respBody),
},
},
}, nil
}
}
func handleUpdateRoute(client *higress.HigressClient) common.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
arguments := request.Params.Arguments
name, ok := arguments["name"].(string)
if !ok {
return nil, fmt.Errorf("missing or invalid 'name' argument")
}
configurations, ok := arguments["configurations"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("missing or invalid 'configurations' argument")
}
// Get current route configuration to merge with updates
currentBody, err := client.Get(fmt.Sprintf("/v1/routes/%s", name))
if err != nil {
return nil, fmt.Errorf("failed to get current route configuration: %w", err)
}
var response RouteResponse
if err := json.Unmarshal(currentBody, &response); err != nil {
return nil, fmt.Errorf("failed to parse current route response: %w", err)
}
currentConfig := response.Data
// Update configurations using JSON marshal/unmarshal for type conversion
configBytes, err := json.Marshal(configurations)
if err != nil {
return nil, fmt.Errorf("failed to marshal configurations: %w", err)
}
var newConfig Route
if err := json.Unmarshal(configBytes, &newConfig); err != nil {
return nil, fmt.Errorf("failed to parse route configurations: %w", err)
}
// Merge configurations (overwrite with new values where provided)
if newConfig.Domains != nil {
currentConfig.Domains = newConfig.Domains
}
if newConfig.Path != nil {
currentConfig.Path = newConfig.Path
}
if newConfig.Methods != nil {
currentConfig.Methods = newConfig.Methods
}
if newConfig.Headers != nil {
currentConfig.Headers = newConfig.Headers
}
if newConfig.URLParams != nil {
currentConfig.URLParams = newConfig.URLParams
}
if newConfig.Services != nil {
currentConfig.Services = newConfig.Services
}
if newConfig.AuthConfig != nil {
currentConfig.AuthConfig = newConfig.AuthConfig
}
if newConfig.CustomConfigs != nil {
currentConfig.CustomConfigs = newConfig.CustomConfigs
}
respBody, err := client.Put(fmt.Sprintf("/v1/routes/%s", name), currentConfig)
if err != nil {
return nil, fmt.Errorf("failed to update route '%s': %w", name, err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(respBody),
},
},
}, nil
}
}
func handleDeleteRoute(client *higress.HigressClient) common.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
arguments := request.Params.Arguments
name, ok := arguments["name"].(string)
if !ok {
return nil, fmt.Errorf("missing or invalid 'name' argument")
}
respBody, err := client.Delete(fmt.Sprintf("/v1/routes/%s", name))
if err != nil {
return nil, fmt.Errorf("failed to delete route '%s': %w", name, err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(respBody),
},
},
}, nil
}
}
func getRouteSchema() json.RawMessage {
return json.RawMessage(`{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the route"
}
},
"required": ["name"],
"additionalProperties": false
}`)
}
func getAddRouteSchema() json.RawMessage {
return json.RawMessage(`{
"type": "object",
"properties": {
"configurations": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the route"
},
"domains": {
"type": "array",
"items": {"type": "string"},
"description": "List of domain names, but only one domain is allowed"
},
"path": {
"type": "object",
"properties": {
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of path"},
"matchValue": {"type": "string", "description": "Value to match"},
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"}
},
"required": ["matchType", "matchValue"],
"description": "List of path match conditions"
},
"methods": {
"type": "array",
"items": {"type": "string", "enum": ["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE", "CONNECT"]},
"description": "List of HTTP methods"
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of header"},
"matchValue": {"type": "string", "description": "Value to match"},
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"},
"key": {"type": "string", "description": "Header key name"}
},
"required": ["matchType", "matchValue", "key"]
},
"description": "List of header match conditions"
},
"urlParams": {
"type": "array",
"items": {
"type": "object",
"properties": {
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of URL parameter"},
"matchValue": {"type": "string", "description": "Value to match"},
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"},
"key": {"type": "string", "description": "Parameter key name"}
},
"required": ["matchType", "matchValue", "key"]
},
"description": "List of URL parameter match conditions"
},
"services": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Service name"},
"port": {"type": "integer", "description": "Service port"},
"weight": {"type": "integer", "description": "Service weight"}
},
"required": ["name", "port", "weight"]
},
"description": "List of services for this route"
},
"customConfigs": {
"type": "object",
"additionalProperties": {"type": "string"},
"description": "Dictionary of custom configurations"
}
},
"required": ["name", "path", "services"],
"additionalProperties": false
}
},
"required": ["configurations"],
"additionalProperties": false
}`)
}
func getUpdateRouteSchema() json.RawMessage {
return json.RawMessage(`{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the route"
},
"configurations": {
"type": "object",
"properties": {
"domains": {
"type": "array",
"items": {"type": "string"},
"description": "List of domain names, but only one domain is allowed",
"maxItems": 1
},
"path": {
"type": "object",
"properties": {
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of path"},
"matchValue": {"type": "string", "description": "Value to match"},
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"}
},
"required": ["matchType", "matchValue"],
"description": "The path configuration"
},
"methods": {
"type": "array",
"items": {"type": "string", "enum": ["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE", "CONNECT"]},
"description": "List of HTTP methods"
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of header"},
"matchValue": {"type": "string", "description": "Value to match"},
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"},
"key": {"type": "string", "description": "Header key name"}
},
"required": ["matchType", "matchValue", "key"]
},
"description": "List of header match conditions"
},
"urlParams": {
"type": "array",
"items": {
"type": "object",
"properties": {
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of URL parameter"},
"matchValue": {"type": "string", "description": "Value to match"},
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"},
"key": {"type": "string", "description": "Parameter key name"}
},
"required": ["matchType", "matchValue", "key"]
},
"description": "List of URL parameter match conditions"
},
"services": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Service name"},
"port": {"type": "integer", "description": "Service port"},
"weight": {"type": "integer", "description": "Service weight"}
},
"required": ["name", "port", "weight"]
},
"description": "List of services for this route"
},
"customConfigs": {
"type": "object",
"additionalProperties": {"type": "string"},
"description": "Dictionary of custom configurations"
}
},
"additionalProperties": false
}
},
"required": ["name", "configurations"],
"additionalProperties": false
}`)
}

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