Compare commits

..

199 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
631 changed files with 65354 additions and 7404 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.5/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,6 +10,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)
[![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>
@@ -68,6 +69,10 @@ 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).
@@ -140,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

@@ -1 +1 @@
v2.1.2-rc.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:
@@ -265,12 +286,23 @@ spec:
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:
@@ -294,6 +326,8 @@ spec:
type: integer
protocol:
type: string
proxyName:
type: string
sni:
type: string
type:

View File

@@ -65,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() {
@@ -106,33 +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"`
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"`
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() {
@@ -321,6 +333,168 @@ func (x *RegistryConfig) GetEnableMCPServer() *wrappers.BoolValue {
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{
@@ -333,72 +507,119 @@ var file_networking_v1_mcp_bridge_proto_rawDesc = []byte{
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, 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,
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, 0x52, 0x0a,
0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0xfd, 0x06, 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, 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,
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 (
@@ -413,20 +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
(*wrappers.BoolValue)(nil), // 2: google.protobuf.BoolValue
(*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
2, // 1: higress.networking.v1.RegistryConfig.enableMCPServer:type_name -> google.protobuf.BoolValue
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] 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() }
@@ -459,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{
@@ -466,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

@@ -46,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 {
@@ -71,4 +72,21 @@ message RegistryConfig {
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 . ); )

4
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
@@ -202,6 +202,7 @@ 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
@@ -274,6 +275,5 @@ replace github.com/caddyserver/certmagic => github.com/2456868764/certmagic v1.0
replace (
github.com/dubbogo/gost => github.com/johnlanni/gost v1.11.23-0.20220713132522-0967a24036c6
github.com/nacos-group/nacos-sdk-go/v2 => github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-60
golang.org/x/exp => golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
)

4
go.sum
View File

@@ -1434,8 +1434,6 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhn
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-60 h1:FA/azfz2nSkMc1XR8LeqhcAiA/2/sOMcyBGYCTUc+Cs=
github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-60/go.mod h1:9FKXl6FqOiVmm72i8kADtbeK71egyG9y3uRDBg41tpQ=
github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
github.com/lyft/protoc-gen-star/v2 v2.0.3/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk=
@@ -1525,6 +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.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=

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 2.1.2-rc.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.2-rc.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.2-rc.1
version: 2.1.7
- name: higress-console
repository: https://higress.io/helm-charts/
version: 2.1.2
digest: sha256:0933dd97b646ae2e1740ccfb6606793b088abeeeb03f687a94c02fe34f227d87
generated: "2025-04-28T20:34:11.26913+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.2-rc.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.2-rc.1
version: 2.1.7
- name: higress-console
repository: "https://higress.io/helm-charts/"
version: 2.1.2
version: 2.1.7
type: application
version: 2.1.2-rc.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

@@ -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

@@ -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"
)
@@ -158,6 +159,8 @@ type IngressConfig struct {
// secretConfigMgr manages secret dependencies
secretConfigMgr *SecretConfigMgr
mcpServerCache mcpserver.McpServerCache
}
// getSecretValue implements the getValue function for secret references
@@ -224,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
@@ -336,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
@@ -421,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
}
@@ -482,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
@@ -590,7 +610,7 @@ func (m *IngressConfig) convertVirtualService(configs []common.WrapperConfig) []
Spec: vs,
})
}
// add vs from naco3 for mcp server
// add vs from nacos3 for mcp server
if m.RegistryReconciler != nil {
allConfigsFromMcp := m.RegistryReconciler.GetAllConfigs(gvk.VirtualService)
for _, cfg := range allConfigsFromMcp {
@@ -608,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") {
@@ -627,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
@@ -661,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))
@@ -794,23 +834,38 @@ func (m *IngressConfig) convertDestinationRule(configs []common.WrapperConfig) [
if !exist {
destinationRules[serviceName] = destinationRuleWrapper
} else if dr.DestinationRule.TrafficPolicy != nil {
if dr.DestinationRule.TrafficPolicy.LoadBalancer == nil &&
destinationRuleWrapper.DestinationRule.TrafficPolicy.LoadBalancer != nil {
dr.DestinationRule.TrafficPolicy.LoadBalancer = destinationRuleWrapper.DestinationRule.TrafficPolicy.LoadBalancer
}
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)
}
}
}
@@ -1090,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)
@@ -1132,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)
}
}
@@ -1188,29 +1243,29 @@ func (m *IngressConfig) AddOrUpdateMcpBridge(clusterNamespacedName util.ClusterN
}
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)
}
for _, f := range m.virtualServiceHandlers {
IngressLog.Debug("McpBridge triggerd virtualservice update")
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 triggerd wasmplugin update")
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 triggerd envoyfilter update")
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
m.configmapMgr.SetMcpReconciler(m.RegistryReconciler)
err = reconciler.Reconcile(mcpbridge)
if err != nil {
IngressLog.Errorf("Mcpbridge reconcile failed, err:%v", err)
@@ -1272,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,
@@ -1470,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{
@@ -1652,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{}) {
@@ -1776,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

@@ -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,7 +18,6 @@ import (
"reflect"
"sync/atomic"
"github.com/alibaba/higress/registry/reconcile"
"istio.io/istio/pilot/pkg/model"
"istio.io/istio/pkg/cluster"
"istio.io/istio/pkg/config"
@@ -33,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"
)
@@ -59,7 +59,6 @@ type ItemController interface {
ValidHigressConfig(higressConfig *HigressConfig) error
ConstructEnvoyFilters() ([]*config.Config, error)
RegisterItemEventHandler(eventHandler ItemEventHandler)
RegisterMcpReconciler(reconciler *reconcile.Reconciler)
}
type ConfigmapMgr struct {
@@ -113,9 +112,11 @@ func (c *ConfigmapMgr) GetHigressConfig() *HigressConfig {
return nil
}
func (c *ConfigmapMgr) SetMcpReconciler(reconciler *reconcile.Reconciler) {
func (c *ConfigmapMgr) RegisterMcpServerProvider(provider mcpserver.McpServerProvider) {
for _, itemController := range c.ItemControllers {
itemController.RegisterMcpReconciler(reconciler)
if mcpRouteProviderAware, ok := itemController.(mcpserver.McpRouteProviderAware); ok {
mcpRouteProviderAware.RegisterMcpServerProvider(provider)
}
}
}
@@ -157,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
@@ -176,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

@@ -21,7 +21,6 @@ import (
"github.com/alibaba/higress/pkg/ingress/kube/util"
. "github.com/alibaba/higress/pkg/ingress/log"
"github.com/alibaba/higress/registry/reconcile"
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/schema/gvk"
@@ -377,9 +376,6 @@ func (g *GlobalOptionController) RegisterItemEventHandler(eventHandler ItemEvent
g.eventHandler = eventHandler
}
func (g *GlobalOptionController) RegisterMcpReconciler(reconciler *reconcile.Reconciler) {
}
// generateDownstreamEnvoyFilter generates the downstream envoy filter.
func (g *GlobalOptionController) generateDownstreamEnvoyFilter(downstreamValueStruct string, bufferLimitStruct string, routeTimeoutStruct string, namespace string) []*networking.EnvoyFilter_EnvoyConfigObjectPatch {
var downstreamConfig []*networking.EnvoyFilter_EnvoyConfigObjectPatch

View File

@@ -23,7 +23,6 @@ import (
"github.com/alibaba/higress/pkg/ingress/kube/util"
. "github.com/alibaba/higress/pkg/ingress/log"
"github.com/alibaba/higress/registry/reconcile"
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/schema/gvk"
@@ -292,9 +291,6 @@ func (g *GzipController) RegisterItemEventHandler(eventHandler ItemEventHandler)
g.eventHandler = eventHandler
}
func (g *GzipController) RegisterMcpReconciler(reconciler *reconcile.Reconciler) {
}
func (g *GzipController) constructGzipStruct(gzip *Gzip, namespace string) string {
gzipConfig := ""
contentType := ""

View File

@@ -22,12 +22,13 @@ import (
"strings"
"sync/atomic"
"github.com/alibaba/higress/pkg/ingress/kube/util"
. "github.com/alibaba/higress/pkg/ingress/log"
"github.com/alibaba/higress/registry/reconcile"
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
@@ -56,7 +57,7 @@ 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"`
@@ -74,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
@@ -83,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
@@ -118,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")
}
}
}
@@ -174,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
@@ -201,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,
}
}
}
@@ -212,18 +233,19 @@ func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
}
type McpServerController struct {
Namespace string
mcpServer atomic.Value
Name string
eventHandler ItemEventHandler
reconclier *reconcile.Reconciler
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
@@ -290,8 +312,11 @@ func (m *McpServerController) RegisterItemEventHandler(eventHandler ItemEventHan
m.eventHandler = eventHandler
}
func (m *McpServerController) RegisterMcpReconciler(reconciler *reconcile.Reconciler) {
m.reconclier = reconciler
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) {
@@ -386,37 +411,47 @@ func (m *McpServerController) ConstructEnvoyFilters() ([]*config.Config, error)
func (m *McpServerController) constructMcpSessionStruct(mcp *McpServer) string {
// Build match_list configuration
matchList := "[]"
var matchConfigs []string
if len(mcp.MatchList) > 0 {
for _, rule := range mcp.MatchList {
matchConfigs = append(matchConfigs, fmt.Sprintf(`{
"match_rule_domain": "%s",
"match_rule_path": "%s",
"match_rule_type": "%s"
}`, rule.MatchRuleDomain, rule.MatchRulePath, rule.MatchRuleType))
var matchList []*MatchRule
matchList = append(matchList, mcp.MatchList...)
for provider, _ := range m.mcpServerProviders {
servers := provider.GetMcpServers()
if len(servers) == 0 {
continue
}
}
if m.reconclier != nil {
vsFromMcp := m.reconclier.GetAllConfigs(gvk.VirtualService)
for _, c := range vsFromMcp {
vs := c.Spec.(*networking.VirtualService)
var host string
if len(vs.Hosts) > 1 {
host = fmt.Sprintf("(%s)", strings.Join(vs.Hosts, "|"))
} else {
host = vs.Hosts[0]
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]
}
}
path := vs.Http[0].Match[0].Uri.GetPrefix()
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": "prefix"
}`, host, path))
"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))
}
matchListConfig = fmt.Sprintf("[%s]", strings.Join(matchConfigs, ","))
}
matchList = fmt.Sprintf("[%s]", strings.Join(matchConfigs, ","))
// Build redis configuration
redisConfig := "null"
@@ -468,12 +503,17 @@ func (m *McpServerController) constructMcpSessionStruct(mcp *McpServer) string {
}`,
redisConfig,
rateLimitConfig,
mcp.SsePathSuffix,
matchList,
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 {

View File

@@ -54,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{
@@ -76,7 +131,7 @@ func Test_validMcpServer(t *testing.T) {
Password: "password",
DB: 0,
},
SsePathSuffix: "/sse",
SSEPathSuffix: "/sse",
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
@@ -238,7 +293,7 @@ func Test_deepCopyMcpServer(t *testing.T) {
Password: "password",
DB: 0,
},
SsePathSuffix: "/sse",
SSEPathSuffix: "/sse",
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
@@ -265,7 +320,7 @@ func Test_deepCopyMcpServer(t *testing.T) {
Password: "password",
DB: 0,
},
SsePathSuffix: "/sse",
SSEPathSuffix: "/sse",
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
@@ -511,7 +566,7 @@ func TestMcpServerController_ConstructEnvoyFilters(t *testing.T) {
MatchList: []*MatchRule{},
Servers: []*SSEServer{},
},
wantConfigs: 2, // Both session and server filters
wantConfigs: 1, // Only session filter when no servers configured
wantErr: nil,
},
}
@@ -581,13 +636,27 @@ func TestMcpServerController_constructMcpSessionStruct(t *testing.T) {
Password: "pass",
DB: 1,
},
SsePathSuffix: "/sse",
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{
@@ -623,7 +692,24 @@ func TestMcpServerController_constructMcpSessionStruct(t *testing.T) {
"match_list": [{
"match_rule_domain": "*",
"match_rule_path": "/test",
"match_rule_type": "exact"
"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
}
@@ -658,24 +744,7 @@ func TestMcpServerController_constructMcpServerStruct(t *testing.T) {
mcp: &McpServer{
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-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": []
}
}
}
}
}`,
wantJSON: "", // Return empty string when no servers configured
},
{
name: "with servers",

View File

@@ -21,7 +21,6 @@ import (
"reflect"
"sync/atomic"
"github.com/alibaba/higress/registry/reconcile"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/schema/gvk"
@@ -238,9 +237,6 @@ func (t *TracingController) RegisterItemEventHandler(eventHandler ItemEventHandl
t.eventHandler = eventHandler
}
func (t *TracingController) RegisterMcpReconciler(reconciler *reconcile.Reconciler) {
}
func (t *TracingController) ConstructEnvoyFilters() ([]*config.Config, error) {
configs := make([]*config.Config, 0)
tracing := t.GetTracing()

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

@@ -17,7 +17,7 @@ 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
@@ -30,7 +30,7 @@ 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

@@ -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: "./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
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

@@ -2,7 +2,7 @@ module github.com/alibaba/higress/plugins/golang-filter
go 1.22
replace github.com/envoyproxy/envoy => github.com/higress-group/envoy v0.0.0-20250428030521-17cf01d9f644
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

View File

@@ -234,8 +234,8 @@ 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-20250428030521-17cf01d9f644 h1:wiLDdiOT3BcTQSFs8oTMu54GIiPFSwKLuWo5J0Cd9b8=
github.com/higress-group/envoy v0.0.0-20250428030521-17cf01d9f644/go.mod h1:SU+IJUAfh1kkZtH+u0E1dnwho8AhbGeYMgp5vvjU+Gc=
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=

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

@@ -0,0 +1,60 @@
# MCP Server
English | [简体中文](./README.md)
## Overview
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:
- 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
> **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
// Parameter 1: Server name
// Parameter 2: Configuration struct instance
func init() {
common.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
}
// Server configuration struct
type DemoConfig struct {
helloworld string
}
// 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)
if !ok { return errors.New("missing helloworld")}
c.helloworld = helloworld
return nil
}
// Create a new MCP server instance
// serverName: Server name
// Returns: MCP server instance and possible error
func (c *DBConfig) NewServer(serverName string) (*common.MCPServer, error) {
mcpServer := common.NewMCPServer(serverName, Version)
// Add tool methods to the server
// mcpServer.AddTool()
// Add resources to the server
// mcpServer.AddResource()
return mcpServer, nil
}
```
**Note**:
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

@@ -5,6 +5,7 @@ import (
_ "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"
@@ -99,12 +100,11 @@ 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, &SSEServerWrapper{
BaseServer: common.NewSSEServer(serverInstance,
common.WithRedisClient(common.GlobalRedisClient),
common.WithSSEEndpoint(fmt.Sprintf("%s%s", serverPath, mcp_session.GlobalSSEPathSuffix)),
common.WithMessageEndpoint(serverPath)),
DomainList: serverDomainList,

View File

@@ -57,12 +57,12 @@ func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.
}
func (f *filter) DecodeData(buffer api.BufferInstance, endStream bool) api.StatusType {
if !endStream {
return api.StopAndBuffer
}
if f.message {
for _, server := range f.config.servers {
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

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

@@ -35,7 +35,7 @@ func (l *McpServerToolsChangeListener) OnToolChanged(reg registry.McpServerRegis
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,
@@ -143,7 +143,7 @@ func (c *NacosConfig) NewServer(serverName string) (*common.MCPServer, error) {
"1.0.0",
)
nacosRegistry, err := CreateNacosMcpRegsitry(c)
nacosRegistry, err := CreateNacosMcpRegistry(c)
if err != nil {
return nil, fmt.Errorf("failed to initialize NacosMcpRegistry: %w", err)
}
@@ -172,7 +172,7 @@ func (c *NacosConfig) NewServer(serverName string) (*common.MCPServer, error) {
func resetToolsToMcpServer(mcpServer *common.MCPServer, reg registry.McpServerRegistry) {
wrappedTools := []common.ServerTool{}
tools := reg.ListToolsDesciption()
tools := reg.ListToolsDescription()
for _, tool := range tools {
wrappedTools = append(wrappedTools, common.ServerTool{
Tool: mcp.NewToolWithRawSchema(tool.Name, tool.Description, tool.InputSchema),

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

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

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

@@ -49,11 +49,24 @@ func (c *DBConfig) NewServer(serverName string) (*common.MCPServer, error) {
)
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

@@ -18,27 +18,80 @@ func HandleQueryTool(dbClient *DBClient) common.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
}`)
}

View File

@@ -0,0 +1,355 @@
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"
)
// ServiceSource represents a service source configuration
type ServiceSource struct {
Name string `json:"name"`
Version string `json:"version,omitempty"`
Type string `json:"type"`
Domain string `json:"domain"`
Port int `json:"port"`
Protocol string `json:"protocol,omitempty"`
SNI *string `json:"sni,omitempty"`
Properties map[string]interface{} `json:"properties,omitempty"`
AuthN *ServiceSourceAuthN `json:"authN,omitempty"`
Valid bool `json:"valid,omitempty"`
}
// ServiceSourceAuthN represents authentication configuration for service source
type ServiceSourceAuthN struct {
Enabled bool `json:"enabled"`
Properties map[string]interface{} `json:"properties,omitempty"`
}
// ServiceSourceResponse represents the API response for service source operations
type ServiceSourceResponse = higress.APIResponse[ServiceSource]
// RegisterServiceTools registers all service source management tools
func RegisterServiceTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
// List all service sources
mcpServer.AddTool(
mcp.NewTool("list-service-sources", mcp.WithDescription("List all available service sources")),
handleListServiceSources(client),
)
// Get specific service source
mcpServer.AddTool(
mcp.NewToolWithRawSchema("get-service-source", "Get detailed information about a specific service source", getServiceSourceSchema()),
handleGetServiceSource(client),
)
// Add new service source
mcpServer.AddTool(
mcp.NewToolWithRawSchema("add-service-source", "Add a new service source", getAddServiceSourceSchema()),
handleAddServiceSource(client),
)
// Update existing service source
mcpServer.AddTool(
mcp.NewToolWithRawSchema("update-service-source", "Update an existing service source", getUpdateServiceSourceSchema()),
handleUpdateServiceSource(client),
)
// Delete existing service source
mcpServer.AddTool(
mcp.NewToolWithRawSchema("delete-service-source", "Delete an existing service source", getServiceSourceSchema()),
handleDeleteServiceSource(client),
)
}
func handleListServiceSources(client *higress.HigressClient) common.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
respBody, err := client.Get("/v1/service-sources")
if err != nil {
return nil, fmt.Errorf("failed to list service sources: %w", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(respBody),
},
},
}, nil
}
}
func handleGetServiceSource(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/service-sources/%s", name))
if err != nil {
return nil, fmt.Errorf("failed to get service source '%s': %w", name, err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(respBody),
},
},
}, nil
}
}
func handleAddServiceSource(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["type"]; !ok {
return nil, fmt.Errorf("missing required field 'type' in configurations")
}
if _, ok := configurations["domain"]; !ok {
return nil, fmt.Errorf("missing required field 'domain' in configurations")
}
if _, ok := configurations["port"]; !ok {
return nil, fmt.Errorf("missing required field 'port' in configurations")
}
respBody, err := client.Post("/v1/service-sources", configurations)
if err != nil {
return nil, fmt.Errorf("failed to add service source: %w", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(respBody),
},
},
}, nil
}
}
func handleUpdateServiceSource(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 service source configuration to merge with updates
currentBody, err := client.Get(fmt.Sprintf("/v1/service-sources/%s", name))
if err != nil {
return nil, fmt.Errorf("failed to get current service source configuration: %w", err)
}
var response ServiceSourceResponse
if err := json.Unmarshal(currentBody, &response); err != nil {
return nil, fmt.Errorf("failed to parse current service source 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 ServiceSource
if err := json.Unmarshal(configBytes, &newConfig); err != nil {
return nil, fmt.Errorf("failed to parse service source configurations: %w", err)
}
// Merge configurations (overwrite with new values where provided)
if newConfig.Name != "" {
currentConfig.Name = newConfig.Name
}
if newConfig.Type != "" {
currentConfig.Type = newConfig.Type
}
if newConfig.Domain != "" {
currentConfig.Domain = newConfig.Domain
}
if newConfig.Port != 0 {
currentConfig.Port = newConfig.Port
}
if newConfig.Protocol != "" {
currentConfig.Protocol = newConfig.Protocol
}
if newConfig.SNI != nil {
currentConfig.SNI = newConfig.SNI
}
if newConfig.Properties != nil {
currentConfig.Properties = newConfig.Properties
}
if newConfig.AuthN != nil {
currentConfig.AuthN = newConfig.AuthN
}
respBody, err := client.Put(fmt.Sprintf("/v1/service-sources/%s", name), currentConfig)
if err != nil {
return nil, fmt.Errorf("failed to update service source '%s': %w", name, err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(respBody),
},
},
}, nil
}
}
func handleDeleteServiceSource(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/service-sources/%s", name))
if err != nil {
return nil, fmt.Errorf("failed to delete service source '%s': %w", name, err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(respBody),
},
},
}, nil
}
}
func getServiceSourceSchema() json.RawMessage {
return json.RawMessage(`{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the service source to retrieve"
}
},
"required": ["name"],
"additionalProperties": false
}`)
}
// TODO: extend other types of service sources, e.g., nacos, zookeeper, euraka.
func getAddServiceSourceSchema() json.RawMessage {
return json.RawMessage(`{
"type": "object",
"properties": {
"configurations": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the service source"
},
"type": {
"type": "string",
"enum": ["static", "dns"],
"description": "The type of service source: 'static' for static IPs, 'dns' for DNS resolution"
},
"domain": {
"type": "string",
"description": "The domain name or IP address (required)"
},
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"description": "The port number (required)"
},
"protocol": {
"type": "string",
"enum": ["http", "https"],
"description": "The protocol to use (optional, defaults to http)"
},
"sni": {
"type": "string",
"description": "Server Name Indication for HTTPS connections (optional)"
}
},
"required": ["name", "type", "domain", "port"],
"additionalProperties": false
}
},
"required": ["configurations"],
"additionalProperties": false
}`)
}
// TODO: extend other types of service sources, e.g., nacos, zookeeper, euraka.
func getUpdateServiceSourceSchema() json.RawMessage {
return json.RawMessage(`{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the service source to update"
},
"configurations": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["static", "dns"],
"description": "The type of service source: 'static' for static IPs, 'dns' for DNS resolution"
},
"domain": {
"type": "string",
"description": "The domain name or IP address"
},
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"description": "The port number"
},
"protocol": {
"type": "string",
"enum": ["http", "https"],
"description": "The protocol to use (optional, defaults to http)"
},
"sni": {
"type": "string",
"description": "Server Name Indication for HTTPS connections"
}
},
"additionalProperties": false
}
},
"required": ["name", "configurations"],
"additionalProperties": false
}`)
}

View File

@@ -0,0 +1,8 @@
package higress
// APIResponse represents the standard Higress API response format
type APIResponse[T any] struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Data T `json:"data,omitempty"`
}

View File

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

View File

@@ -3,24 +3,36 @@ package common
import (
"regexp"
"strings"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
)
// RuleType defines the type of matching rule
type RuleType string
// UpstreamType defines the type of matching rule
type UpstreamType string
const (
ExactMatch RuleType = "exact"
PrefixMatch RuleType = "prefix"
SuffixMatch RuleType = "suffix"
ContainsMatch RuleType = "contains"
RegexMatch RuleType = "regex"
RestUpstream UpstreamType = "rest"
SSEUpstream UpstreamType = "sse"
StreamableUpstream UpstreamType = "streamable"
)
// 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
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
UpstreamType UpstreamType `json:"upstream_type"` // Type of upstream(s) matched by the rule
EnablePathRewrite bool `json:"enable_path_rewrite"` // Enable request path rewrite for matched routes
PathRewritePrefix string `json:"path_rewrite_prefix"` // Prefix the request path would be rewritten to.
}
// ParseMatchList parses the match list from the config
@@ -38,6 +50,34 @@ func ParseMatchList(matchListConfig []interface{}) []MatchRule {
if ruleType, ok := ruleMap["match_rule_type"].(string); ok {
rule.MatchRuleType = RuleType(ruleType)
}
if upstreamType, ok := ruleMap["upstream_type"].(string); ok {
rule.UpstreamType = UpstreamType(upstreamType)
}
if len(rule.UpstreamType) == 0 {
rule.UpstreamType = RestUpstream
} else {
switch rule.UpstreamType {
case RestUpstream, SSEUpstream, StreamableUpstream:
break
default:
api.LogWarnf("Unknown upstream type: %s", rule.UpstreamType)
}
}
if enablePathRewrite, ok := ruleMap["enable_path_rewrite"].(bool); ok {
rule.EnablePathRewrite = enablePathRewrite
}
if pathRewritePrefix, ok := ruleMap["path_rewrite_prefix"].(string); ok {
rule.PathRewritePrefix = pathRewritePrefix
}
if rule.EnablePathRewrite {
if rule.UpstreamType != SSEUpstream {
api.LogWarnf("Path rewrite is only supported for SSE upstream type")
} else if rule.MatchRuleType != PrefixMatch {
api.LogWarnf("Path rewrite is only supported for prefix match type")
} else if !strings.HasPrefix(rule.PathRewritePrefix, "/") {
rule.PathRewritePrefix = "/" + rule.PathRewritePrefix
}
}
matchList = append(matchList, rule)
}
}
@@ -96,17 +136,17 @@ func matchDomainAndPath(domain, path string, rule MatchRule) bool {
// 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 {
func IsMatch(rules []MatchRule, host, path string) (bool, MatchRule) {
if len(rules) == 0 {
return true
return true, MatchRule{}
}
for _, rule := range rules {
if matchDomainAndPath(host, path, rule) {
return true
return true, rule
}
}
return false
return false, MatchRule{}
}
// MatchDomainList checks if the domain matches any of the domains in the list

View File

@@ -9,8 +9,6 @@ import (
"github.com/go-redis/redis/v8"
)
var GlobalRedisClient *RedisClient
type RedisConfig struct {
address string
username string
@@ -74,9 +72,10 @@ func NewRedisClient(config *RedisConfig) (*RedisClient, error) {
// Ping the Redis server to check the connection
pong, err := client.Ping(context.Background()).Result()
if err != nil {
return nil, fmt.Errorf("failed to connect to Redis: %w", err)
api.LogErrorf("Failed to connect to Redis: %v", err)
} else {
api.LogDebugf("Connected to Redis: %s", pong)
}
api.LogDebugf("Connected to Redis: %s", pong)
ctx, cancel := context.WithCancel(context.Background())
@@ -85,7 +84,7 @@ func NewRedisClient(config *RedisConfig) (*RedisClient, error) {
crypto, err = NewCrypto(config.secret)
if err != nil {
cancel()
return nil, err
api.LogWarnf("Failed to initialize redis crypto: %v", err)
}
}
@@ -105,7 +104,7 @@ func NewRedisClient(config *RedisConfig) (*RedisClient, error) {
// keepAlive periodically checks Redis connection and attempts to reconnect if needed
func (r *RedisClient) keepAlive() {
ticker := time.NewTicker(30 * time.Second)
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {

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