mirror of
https://github.com/alibaba/higress.git
synced 2026-02-26 05:30:50 +08:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48b220453b | ||
|
|
489a800868 | ||
|
|
60c9f21e1c | ||
|
|
ab73f21017 | ||
|
|
806563298b | ||
|
|
02fabbb35f | ||
|
|
07154d1f49 | ||
|
|
db30c0962a | ||
|
|
731fe43d14 | ||
|
|
5bd20aa559 | ||
|
|
a2e4f944e9 | ||
|
|
7955aec639 | ||
|
|
e12feb9f57 | ||
|
|
03b4144cff | ||
|
|
c382635e7f | ||
|
|
e381806ba0 | ||
|
|
52114b37f8 | ||
|
|
b4e68c02f9 | ||
|
|
c241ccf19d | ||
|
|
e4fa1e6390 | ||
|
|
b103b9d7cb | ||
|
|
90b02a90e0 | ||
|
|
38f718b965 | ||
|
|
8752a763c2 | ||
|
|
a57173ce28 | ||
|
|
3a8d8f5b94 | ||
|
|
1c37c361e1 | ||
|
|
b8133a95b2 | ||
|
|
36d5d391b8 | ||
|
|
1da9a07866 | ||
|
|
8620838f8b | ||
|
|
e7d2005382 |
@@ -144,7 +144,7 @@ docker-buildx-push: clean-env docker.higress-buildx
|
||||
export PARENT_GIT_TAG:=$(shell cat VERSION)
|
||||
export PARENT_GIT_REVISION:=$(TAG)
|
||||
|
||||
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.1.4/envoy-symbol-ARCH.tar.gz
|
||||
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.1.5/envoy-symbol-ARCH.tar.gz
|
||||
|
||||
build-envoy: prebuild
|
||||
./tools/hack/build-envoy.sh
|
||||
|
||||
49
README.md
49
README.md
@@ -11,7 +11,8 @@
|
||||
[](https://github.com/alibaba/higress/actions)
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||
|
||||
<a href="https://trendshift.io/repositories/10918" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10918" alt="alibaba%2Fhigress | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
<a href="https://trendshift.io/repositories/10918" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10918" alt="alibaba%2Fhigress | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://www.producthunt.com/posts/higress?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-higress" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=951287&theme=light&t=1745492822283" alt="Higress - Global APIs as MCP powered by AI Gateway | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
</div>
|
||||
|
||||
[**Official Site**](https://higress.ai/en/) |
|
||||
@@ -22,13 +23,21 @@
|
||||
English | <a href="README_ZH.md">中文<a/> | <a href="README_JP.md">日本語<a/>
|
||||
</p>
|
||||
|
||||
## What is Higress?
|
||||
|
||||
Higress is a cloud-native API gateway based on Istio and Envoy, which can be extended with Wasm plugins written in Go/Rust/JS. It provides dozens of ready-to-use general-purpose plugins and an out-of-the-box console (try the [demo here](http://demo.higress.io/)).
|
||||
|
||||
Higress was born within Alibaba to solve the issues of Tengine reload affecting long-connection services and insufficient load balancing capabilities for gRPC/Dubbo.
|
||||
### Core Use Cases
|
||||
|
||||
Alibaba Cloud has built its cloud-native API gateway product based on Higress, providing 99.99% gateway high availability guarantee service capabilities for a large number of enterprise customers.
|
||||
Higress's AI gateway capabilities support all [mainstream model providers](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider) both domestic and international. It also supports hosting MCP (Model Context Protocol) Servers through its plugin mechanism, enabling AI Agents to easily call various tools and services. With the [openapi-to-mcp tool](https://github.com/higress-group/openapi-to-mcpserver), you can quickly convert OpenAPI specifications into remote MCP servers for hosting. Higress provides unified management for both LLM API and MCP API.
|
||||
|
||||
Higress's AI gateway capabilities support all [mainstream model providers](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider) both domestic and international, as well as self-built DeepSeek models based on vllm/ollama. Within Alibaba Cloud, it supports AI businesses such as Tongyi Qianwen APP, Bailian large model API, and machine learning PAI platform. It also serves leading AIGC enterprises (such as Zero One Infinite) and AI products (such as FastGPT).
|
||||
**🌟 Try it now at [https://mcp.higress.ai/](https://mcp.higress.ai/)** to experience Higress-hosted Remote MCP Servers firsthand:
|
||||
|
||||

|
||||
|
||||
### Enterprise Adoption
|
||||
|
||||
Higress was born within Alibaba to solve the issues of Tengine reload affecting long-connection services and insufficient load balancing capabilities for gRPC/Dubbo. Within Alibaba Cloud, Higress's AI gateway capabilities support core AI applications such as Tongyi Bailian model studio, machine learning PAI platform, and other critical AI services. Alibaba Cloud has built its cloud-native API gateway product based on Higress, providing 99.99% gateway high availability guarantee service capabilities for a large number of enterprise customers.
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -64,32 +73,28 @@ For other installation methods such as Helm deployment under K8s, please refer t
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **AI Gateway**:
|
||||
|
||||
Higress can connect to all LLM model providers both domestic and international using a unified protocol, while also providing rich AI observability, multi-model load balancing/fallback, AI token rate limiting, AI caching, and other capabilities:
|
||||
|
||||

|
||||
|
||||
- **MCP Server Hosting**:
|
||||
|
||||
Higress, as an Envoy-based API gateway, supports hosting MCP Servers through its plugin mechanism. MCP (Model Context Protocol) is essentially an AI-friendly API that enables AI Agents to more easily call various tools and services. Higress provides unified capabilities for authentication, authorization, rate limiting, and observability for tool calls, simplifying the development and deployment of AI applications.
|
||||
Higress hosts MCP Servers through its plugin mechanism, enabling AI Agents to easily call various tools and services. With the [openapi-to-mcp tool](https://github.com/higress-group/openapi-to-mcpserver), you can quickly convert OpenAPI specifications into remote MCP servers.
|
||||
|
||||

|
||||
|
||||
**🌟 Try it now!** Experience Higress-hosted Remote MCP Servers at [https://mcp.higress.ai/](https://mcp.higress.ai/)
|
||||
|
||||

|
||||
|
||||
By hosting MCP Servers with Higress, you can achieve:
|
||||
- Unified authentication and authorization mechanisms, ensuring the security of AI tool calls
|
||||
- Fine-grained rate limiting to prevent abuse and resource exhaustion
|
||||
- Comprehensive audit logs recording all tool call behaviors
|
||||
- Rich observability for monitoring the performance and health of tool calls
|
||||
- Simplified deployment and management through Higress's plugin mechanism for quickly adding new MCP Servers
|
||||
- Dynamic updates without disruption: Thanks to Envoy's friendly handling of long connections and Wasm plugin's dynamic update mechanism, MCP Server logic can be updated on-the-fly without any traffic disruption or connection drops
|
||||
Key benefits of hosting MCP Servers with Higress:
|
||||
- Unified authentication and authorization mechanisms
|
||||
- Fine-grained rate limiting to prevent abuse
|
||||
- Comprehensive audit logs for all tool calls
|
||||
- Rich observability for monitoring performance
|
||||
- Simplified deployment through Higress's plugin mechanism
|
||||
- Dynamic updates without disruption or connection drops
|
||||
|
||||
[Learn more...](https://higress.cn/en/ai/mcp-quick-start/?spm=36971b57.7beea2de.0.0.d85f20a94jsWGm)
|
||||
|
||||
- **AI Gateway**:
|
||||
|
||||
Higress connects to all LLM model providers using a unified protocol, with AI observability, multi-model load balancing, token rate limiting, and caching capabilities:
|
||||
|
||||

|
||||
|
||||
- **Kubernetes ingress controller**:
|
||||
|
||||
Higress can function as a feature-rich ingress controller, which is compatible with many annotations of K8s' nginx ingress controller.
|
||||
|
||||
18
README_JP.md
18
README_JP.md
@@ -22,15 +22,21 @@
|
||||
</p>
|
||||
|
||||
|
||||
## Higressとは?
|
||||
|
||||
Higressは、IstioとEnvoyをベースにしたクラウドネイティブAPIゲートウェイで、Go/Rust/JSなどを使用してWasmプラグインを作成できます。数十の既製の汎用プラグインと、すぐに使用できるコンソールを提供しています(デモは[こちら](http://demo.higress.io/))。
|
||||
|
||||
Higressは、Tengineのリロードが長時間接続のビジネスに影響を与える問題や、gRPC/Dubboの負荷分散能力の不足を解決するために、Alibaba内部で誕生しました。
|
||||
### 主な使用シナリオ
|
||||
|
||||
Alibaba Cloudは、Higressを基盤にクラウドネイティブAPIゲートウェイ製品を構築し、多くの企業顧客に99.99%のゲートウェイ高可用性保証サービスを提供しています。
|
||||
HigressのAIゲートウェイ機能は、国内外のすべての[主要モデルプロバイダー](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider)をサポートし、vllm/ollamaなどに基づく自己構築DeepSeekモデルにも対応しています。また、プラグインメカニズムを通じてMCP(Model Context Protocol)サーバーをホストすることもでき、AI Agentが様々なツールやサービスを簡単に呼び出せるようにします。[openapi-to-mcpツール](https://github.com/higress-group/openapi-to-mcpserver)を使用すると、OpenAPI仕様を迅速にリモートMCPサーバーに変換してホスティングできます。HigressはLLM APIとMCP APIの統一管理を提供します。
|
||||
|
||||
Higressは、AIゲートウェイ機能を基盤に、Tongyi Qianwen APP、Bailian大規模モデルAPI、機械学習PAIプラットフォームなどのAIビジネスをサポートしています。また、国内の主要なAIGC企業(例:ZeroOne)やAI製品(例:FastGPT)にもサービスを提供しています。
|
||||
**🌟 今すぐ[https://mcp.higress.ai/](https://mcp.higress.ai/)で体験**してください。HigressがホストするリモートMCPサーバーを直接体験できます:
|
||||
|
||||

|
||||

|
||||
|
||||
### 企業での採用
|
||||
|
||||
Higressは、Tengineのリロードが長時間接続のビジネスに影響を与える問題や、gRPC/Dubboの負荷分散能力の不足を解決するために、Alibaba内部で誕生しました。Alibaba Cloud内では、HigressのAIゲートウェイ機能がTongyi Qianwen APP、Tongyi Bailian Model Studio、機械学習PAIプラットフォームなどの中核的なAIアプリケーションをサポートしています。また、国内の主要なAIGC企業(例:ZeroOne)やAI製品(例:FastGPT)にもサービスを提供しています。Alibaba Cloudは、Higressを基盤にクラウドネイティブAPIゲートウェイ製品を構築し、多くの企業顧客に99.99%のゲートウェイ高可用性保証サービスを提供しています。
|
||||
|
||||
|
||||
## 目次
|
||||
@@ -79,10 +85,6 @@ K8sでのHelmデプロイなどの他のインストール方法については
|
||||
|
||||

|
||||
|
||||
**🌟 今すぐ試してみよう!** [https://mcp.higress.ai/](https://mcp.higress.ai/) でHigressがホストするリモートMCPサーバーを体験できます。このプラットフォームでは、HigressがどのようにリモートMCPサーバーをホストおよび管理するかを直接体験できます。
|
||||
|
||||

|
||||
|
||||
Higressを使用してMCP Serverをホストすることで、以下のことが実現できます:
|
||||
- 統一された認証と認可メカニズム、AIツール呼び出しのセキュリティを確保
|
||||
- きめ細かいレート制限、乱用やリソース枯渇を防止
|
||||
|
||||
20
README_ZH.md
20
README_ZH.md
@@ -11,7 +11,7 @@
|
||||
[](https://github.com/alibaba/higress/actions)
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||
|
||||
<a href="https://trendshift.io/repositories/10918" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10918" alt="alibaba%2Fhigress | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
<a href="https://trendshift.io/repositories/10918" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10918" alt="alibaba%2Fhigress | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://www.producthunt.com/posts/higress?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-higress" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=951287&theme=light&t=1745492822283" alt="Higress - Global APIs as MCP powered by AI Gateway | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
</div>
|
||||
|
||||
[**官网**](https://higress.cn/) |
|
||||
@@ -28,15 +28,21 @@
|
||||
</p>
|
||||
|
||||
|
||||
## Higress 是什么?
|
||||
|
||||
Higress 是一款云原生 API 网关,内核基于 Istio 和 Envoy,可以用 Go/Rust/JS 等编写 Wasm 插件,提供了数十个现成的通用插件,以及开箱即用的控制台(demo 点[这里](http://demo.higress.io/))
|
||||
|
||||
Higress 在阿里内部为解决 Tengine reload 对长连接业务有损,以及 gRPC/Dubbo 负载均衡能力不足而诞生。
|
||||
### 核心使用场景
|
||||
|
||||
阿里云基于 Higress 构建了云原生 API 网关产品,为大量企业客户提供 99.99% 的网关高可用保障服务能力。
|
||||
Higress 的 AI 网关能力支持国内外所有[主流模型供应商](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider)和基于 vllm/ollama 等自建的 DeepSeek 模型。同时,Higress 支持通过插件方式托管 MCP (Model Context Protocol) 服务器,使 AI Agent 能够更容易地调用各种工具和服务。借助 [openapi-to-mcp 工具](https://github.com/higress-group/openapi-to-mcpserver),您可以快速将 OpenAPI 规范转换为远程 MCP 服务器进行托管。Higress 提供了对 LLM API 和 MCP API 的统一管理。
|
||||
|
||||
Higress 的 AI 网关能力支持国内外所有[主流模型供应商](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider)和基于 vllm/ollama 等自建的 DeepSeek 模型;在阿里云内部支撑了通义千问 APP、百炼大模型 API、机器学习 PAI 平台等 AI 业务。同时服务国内头部的 AIGC 企业(如零一万物),以及 AI 产品(如 FastGPT)
|
||||
**🌟 立即体验 [https://mcp.higress.ai/](https://mcp.higress.ai/)** 基于 Higress 托管的远程 MCP 服务器:
|
||||
|
||||

|
||||

|
||||
|
||||
### 生产环境采用
|
||||
|
||||
Higress 在阿里内部为解决 Tengine reload 对长连接业务有损,以及 gRPC/Dubbo 负载均衡能力不足而诞生。在阿里云内部,Higress 的 AI 网关能力支撑了通义千问 APP、通义百炼模型工作室、机器学习 PAI 平台等核心 AI 应用。同时服务国内头部的 AIGC 企业(如零一万物),以及 AI 产品(如 FastGPT)。阿里云基于 Higress 构建了云原生 API 网关产品,为大量企业客户提供 99.99% 的网关高可用保障服务能力。
|
||||
|
||||
|
||||
## Summary
|
||||
@@ -89,10 +95,6 @@ K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start
|
||||
|
||||

|
||||
|
||||
**🌟 立即体验!** 在 [https://mcp.higress.ai/](https://mcp.higress.ai/) 体验 Higress 托管的远程 MCP 服务器。这个平台让您可以体验基于 Higress 托管的远程 MCP 服务器的效果。
|
||||
|
||||

|
||||
|
||||
通过 Higress 托管 MCP Server,可以实现:
|
||||
- 统一的认证和鉴权机制,确保 AI 工具调用的安全性
|
||||
- 精细化的速率限制,防止滥用和资源耗尽
|
||||
|
||||
@@ -263,6 +263,14 @@ spec:
|
||||
type: string
|
||||
domain:
|
||||
type: string
|
||||
enableMCPServer:
|
||||
type: boolean
|
||||
mcpServerBaseUrl:
|
||||
type: string
|
||||
mcpServerExportDomains:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
nacosAccessKey:
|
||||
type: string
|
||||
nacosAddressServer:
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
_ "github.com/golang/protobuf/ptypes/struct"
|
||||
wrappers "github.com/golang/protobuf/ptypes/wrappers"
|
||||
_ "google.golang.org/genproto/googleapis/api/annotations"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
@@ -109,25 +111,28 @@ type RegistryConfig struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Domain string `protobuf:"bytes,3,opt,name=domain,proto3" json:"domain,omitempty"`
|
||||
Port uint32 `protobuf:"varint,4,opt,name=port,proto3" json:"port,omitempty"`
|
||||
NacosAddressServer string `protobuf:"bytes,5,opt,name=nacosAddressServer,proto3" json:"nacosAddressServer,omitempty"`
|
||||
NacosAccessKey string `protobuf:"bytes,6,opt,name=nacosAccessKey,proto3" json:"nacosAccessKey,omitempty"`
|
||||
NacosSecretKey string `protobuf:"bytes,7,opt,name=nacosSecretKey,proto3" json:"nacosSecretKey,omitempty"`
|
||||
NacosNamespaceId string `protobuf:"bytes,8,opt,name=nacosNamespaceId,proto3" json:"nacosNamespaceId,omitempty"`
|
||||
NacosNamespace string `protobuf:"bytes,9,opt,name=nacosNamespace,proto3" json:"nacosNamespace,omitempty"`
|
||||
NacosGroups []string `protobuf:"bytes,10,rep,name=nacosGroups,proto3" json:"nacosGroups,omitempty"`
|
||||
NacosRefreshInterval int64 `protobuf:"varint,11,opt,name=nacosRefreshInterval,proto3" json:"nacosRefreshInterval,omitempty"`
|
||||
ConsulNamespace string `protobuf:"bytes,12,opt,name=consulNamespace,proto3" json:"consulNamespace,omitempty"`
|
||||
ZkServicesPath []string `protobuf:"bytes,13,rep,name=zkServicesPath,proto3" json:"zkServicesPath,omitempty"`
|
||||
ConsulDatacenter string `protobuf:"bytes,14,opt,name=consulDatacenter,proto3" json:"consulDatacenter,omitempty"`
|
||||
ConsulServiceTag string `protobuf:"bytes,15,opt,name=consulServiceTag,proto3" json:"consulServiceTag,omitempty"`
|
||||
ConsulRefreshInterval int64 `protobuf:"varint,16,opt,name=consulRefreshInterval,proto3" json:"consulRefreshInterval,omitempty"`
|
||||
AuthSecretName string `protobuf:"bytes,17,opt,name=authSecretName,proto3" json:"authSecretName,omitempty"`
|
||||
Protocol string `protobuf:"bytes,18,opt,name=protocol,proto3" json:"protocol,omitempty"`
|
||||
Sni string `protobuf:"bytes,19,opt,name=sni,proto3" json:"sni,omitempty"`
|
||||
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Domain string `protobuf:"bytes,3,opt,name=domain,proto3" json:"domain,omitempty"`
|
||||
Port uint32 `protobuf:"varint,4,opt,name=port,proto3" json:"port,omitempty"`
|
||||
NacosAddressServer string `protobuf:"bytes,5,opt,name=nacosAddressServer,proto3" json:"nacosAddressServer,omitempty"`
|
||||
NacosAccessKey string `protobuf:"bytes,6,opt,name=nacosAccessKey,proto3" json:"nacosAccessKey,omitempty"`
|
||||
NacosSecretKey string `protobuf:"bytes,7,opt,name=nacosSecretKey,proto3" json:"nacosSecretKey,omitempty"`
|
||||
NacosNamespaceId string `protobuf:"bytes,8,opt,name=nacosNamespaceId,proto3" json:"nacosNamespaceId,omitempty"`
|
||||
NacosNamespace string `protobuf:"bytes,9,opt,name=nacosNamespace,proto3" json:"nacosNamespace,omitempty"`
|
||||
NacosGroups []string `protobuf:"bytes,10,rep,name=nacosGroups,proto3" json:"nacosGroups,omitempty"`
|
||||
NacosRefreshInterval int64 `protobuf:"varint,11,opt,name=nacosRefreshInterval,proto3" json:"nacosRefreshInterval,omitempty"`
|
||||
ConsulNamespace string `protobuf:"bytes,12,opt,name=consulNamespace,proto3" json:"consulNamespace,omitempty"`
|
||||
ZkServicesPath []string `protobuf:"bytes,13,rep,name=zkServicesPath,proto3" json:"zkServicesPath,omitempty"`
|
||||
ConsulDatacenter string `protobuf:"bytes,14,opt,name=consulDatacenter,proto3" json:"consulDatacenter,omitempty"`
|
||||
ConsulServiceTag string `protobuf:"bytes,15,opt,name=consulServiceTag,proto3" json:"consulServiceTag,omitempty"`
|
||||
ConsulRefreshInterval int64 `protobuf:"varint,16,opt,name=consulRefreshInterval,proto3" json:"consulRefreshInterval,omitempty"`
|
||||
AuthSecretName string `protobuf:"bytes,17,opt,name=authSecretName,proto3" json:"authSecretName,omitempty"`
|
||||
Protocol string `protobuf:"bytes,18,opt,name=protocol,proto3" json:"protocol,omitempty"`
|
||||
Sni string `protobuf:"bytes,19,opt,name=sni,proto3" json:"sni,omitempty"`
|
||||
McpServerExportDomains []string `protobuf:"bytes,20,rep,name=mcpServerExportDomains,proto3" json:"mcpServerExportDomains,omitempty"`
|
||||
McpServerBaseUrl string `protobuf:"bytes,21,opt,name=mcpServerBaseUrl,proto3" json:"mcpServerBaseUrl,omitempty"`
|
||||
EnableMCPServer *wrappers.BoolValue `protobuf:"bytes,22,opt,name=enableMCPServer,proto3" json:"enableMCPServer,omitempty"`
|
||||
}
|
||||
|
||||
func (x *RegistryConfig) Reset() {
|
||||
@@ -295,6 +300,27 @@ func (x *RegistryConfig) GetSni() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RegistryConfig) GetMcpServerExportDomains() []string {
|
||||
if x != nil {
|
||||
return x.McpServerExportDomains
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *RegistryConfig) GetMcpServerBaseUrl() string {
|
||||
if x != nil {
|
||||
return x.McpServerBaseUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RegistryConfig) GetEnableMCPServer() *wrappers.BoolValue {
|
||||
if x != nil {
|
||||
return x.EnableMCPServer
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_networking_v1_mcp_bridge_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_networking_v1_mcp_bridge_proto_rawDesc = []byte{
|
||||
@@ -303,61 +329,76 @@ var file_networking_v1_mcp_bridge_proto_rawDesc = []byte{
|
||||
0x12, 0x15, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
|
||||
0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f,
|
||||
0x61, 0x70, 0x69, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69,
|
||||
0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x52, 0x0a, 0x09, 0x4d, 0x63, 0x70, 0x42,
|
||||
0x72, 0x69, 0x64, 0x67, 0x65, 0x12, 0x45, 0x0a, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72,
|
||||
0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x68, 0x69, 0x67, 0x72,
|
||||
0x65, 0x73, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76,
|
||||
0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
||||
0x52, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0xd3, 0x05, 0x0a,
|
||||
0x0e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
|
||||
0x17, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0,
|
||||
0x41, 0x02, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x06,
|
||||
0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41,
|
||||
0x02, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x17, 0x0a, 0x04, 0x70, 0x6f, 0x72,
|
||||
0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x04, 0x70, 0x6f,
|
||||
0x72, 0x74, 0x12, 0x2e, 0x0a, 0x12, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x41, 0x64, 0x64, 0x72, 0x65,
|
||||
0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12,
|
||||
0x6e, 0x61, 0x63, 0x6f, 0x73, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76,
|
||||
0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x41, 0x63, 0x63, 0x65, 0x73,
|
||||
0x73, 0x4b, 0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x61, 0x63, 0x6f,
|
||||
0x73, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x61,
|
||||
0x63, 0x6f, 0x73, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b,
|
||||
0x65, 0x79, 0x12, 0x2a, 0x0a, 0x10, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73,
|
||||
0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6e, 0x61,
|
||||
0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x26,
|
||||
0x0a, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65,
|
||||
0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d,
|
||||
0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x47,
|
||||
0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x61, 0x63,
|
||||
0x6f, 0x73, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x6e, 0x61, 0x63, 0x6f,
|
||||
0x73, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c,
|
||||
0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x52, 0x65, 0x66,
|
||||
0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x28, 0x0a, 0x0f,
|
||||
0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18,
|
||||
0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x4e, 0x61, 0x6d,
|
||||
0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x7a, 0x6b, 0x53, 0x65, 0x72, 0x76,
|
||||
0x69, 0x63, 0x65, 0x73, 0x50, 0x61, 0x74, 0x68, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e,
|
||||
0x7a, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x50, 0x61, 0x74, 0x68, 0x12, 0x2a,
|
||||
0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74,
|
||||
0x65, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c,
|
||||
0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x10, 0x63, 0x6f,
|
||||
0x6e, 0x73, 0x75, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x54, 0x61, 0x67, 0x18, 0x0f,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x53, 0x65, 0x72, 0x76,
|
||||
0x69, 0x63, 0x65, 0x54, 0x61, 0x67, 0x12, 0x34, 0x0a, 0x15, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c,
|
||||
0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18,
|
||||
0x10, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x52, 0x65, 0x66,
|
||||
0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x26, 0x0a, 0x0e,
|
||||
0x61, 0x75, 0x74, 0x68, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x11,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74,
|
||||
0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c,
|
||||
0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c,
|
||||
0x12, 0x10, 0x0a, 0x03, 0x73, 0x6e, 0x69, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x73,
|
||||
0x6e, 0x69, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
|
||||
0x2f, 0x61, 0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x2f, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73,
|
||||
0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2f,
|
||||
0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
||||
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65,
|
||||
0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
||||
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74,
|
||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x52, 0x0a, 0x09, 0x4d, 0x63, 0x70, 0x42, 0x72, 0x69,
|
||||
0x64, 0x67, 0x65, 0x12, 0x45, 0x0a, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x69, 0x65,
|
||||
0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73,
|
||||
0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e,
|
||||
0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a,
|
||||
0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 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,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -374,16 +415,18 @@ func file_networking_v1_mcp_bridge_proto_rawDescGZIP() []byte {
|
||||
|
||||
var file_networking_v1_mcp_bridge_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_networking_v1_mcp_bridge_proto_goTypes = []interface{}{
|
||||
(*McpBridge)(nil), // 0: higress.networking.v1.McpBridge
|
||||
(*RegistryConfig)(nil), // 1: higress.networking.v1.RegistryConfig
|
||||
(*McpBridge)(nil), // 0: higress.networking.v1.McpBridge
|
||||
(*RegistryConfig)(nil), // 1: higress.networking.v1.RegistryConfig
|
||||
(*wrappers.BoolValue)(nil), // 2: google.protobuf.BoolValue
|
||||
}
|
||||
var file_networking_v1_mcp_bridge_proto_depIdxs = []int32{
|
||||
1, // 0: higress.networking.v1.McpBridge.registries:type_name -> higress.networking.v1.RegistryConfig
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
2, // 1: higress.networking.v1.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
|
||||
}
|
||||
|
||||
func init() { file_networking_v1_mcp_bridge_proto_init() }
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "google/api/field_behavior.proto";
|
||||
import "google/protobuf/wrappers.proto";
|
||||
import "google/protobuf/struct.proto";
|
||||
|
||||
// $schema: higress.networking.v1.McpBridge
|
||||
// $title: McpBridge
|
||||
@@ -66,4 +68,7 @@ message RegistryConfig {
|
||||
string authSecretName = 17;
|
||||
string protocol = 18;
|
||||
string sni = 19;
|
||||
repeated string mcpServerExportDomains = 20;
|
||||
string mcpServerBaseUrl = 21;
|
||||
google.protobuf.BoolValue enableMCPServer = 22;
|
||||
}
|
||||
|
||||
Submodule envoy/envoy updated: e114a74dd8...17cf01d9f6
39
go.mod
39
go.mod
@@ -39,7 +39,7 @@ require (
|
||||
github.com/tidwall/gjson v1.17.0
|
||||
go.uber.org/atomic v1.11.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/net v0.27.0
|
||||
golang.org/x/net v0.33.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13
|
||||
google.golang.org/grpc v1.59.0
|
||||
google.golang.org/protobuf v1.33.0
|
||||
@@ -71,7 +71,27 @@ require (
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/alecholmes/xfccparser v0.1.0 // indirect
|
||||
github.com/alecthomas/participle v0.4.1 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/darabonba-array v0.1.0 // indirect
|
||||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 // indirect
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2 // indirect
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 // indirect
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 // indirect
|
||||
github.com/alibabacloud-go/darabonba-string v1.0.2 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
|
||||
github.com/alibabacloud-go/kms-20160120/v3 v3.2.3 // indirect
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
|
||||
github.com/alibabacloud-go/tea v1.2.2 // indirect
|
||||
github.com/alibabacloud-go/tea-utils v1.4.4 // indirect
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800 // indirect
|
||||
github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1 // indirect
|
||||
github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8 // indirect
|
||||
github.com/aliyun/aliyun-secretsmanager-client-go v1.1.5 // indirect
|
||||
github.com/aliyun/credentials-go v1.4.3 // indirect
|
||||
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
@@ -82,10 +102,12 @@ require (
|
||||
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/clbanning/mxj v1.8.4 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.5.5 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.6.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/deckarep/golang-set v1.7.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/docker/cli v24.0.7+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.2+incompatible // indirect
|
||||
@@ -165,6 +187,7 @@ require (
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
|
||||
github.com/openshift/api v0.0.0-20230720094506-afcbe27aec7c // indirect
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
@@ -182,6 +205,7 @@ require (
|
||||
github.com/tetratelabs/wazero v1.7.3 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/toolkits/concurrent v0.0.0-20150624120057-a4371d70e3e3 // indirect
|
||||
github.com/vbatts/tar-split v0.11.3 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
@@ -197,14 +221,14 @@ require (
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/oauth2 v0.13.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/term v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/term v0.27.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
@@ -250,5 +274,6 @@ 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
|
||||
)
|
||||
|
||||
144
go.sum
144
go.sum
@@ -683,9 +683,68 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/alessio/shellescape v1.2.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
|
||||
github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
|
||||
github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
|
||||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
|
||||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9/go.mod h1:bb+Io8Sn2RuM3/Rpme6ll86jMyFSrD1bxeV/+v61KeU=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 h1:GEYkMApgpKEVDn6z12DcH1EGYpDYRB8JxsazM4Rywak=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10/go.mod h1:26a14FGhZVELuz2cc2AolvW4RHmIO3/HRwsdHhaIPDE=
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
|
||||
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
|
||||
github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=
|
||||
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
|
||||
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||
github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
|
||||
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
|
||||
github.com/alibabacloud-go/kms-20160120/v3 v3.2.3 h1:vamGcYQFwXVqR6RWcrVTTqlIXZVsYjaA7pZbx+Xw6zw=
|
||||
github.com/alibabacloud-go/kms-20160120/v3 v3.2.3/go.mod h1:3rIyughsFDLie1ut9gQJXkWkMg/NfXBCk+OtXnPu3lw=
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY=
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
|
||||
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
|
||||
github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||
github.com/alibabacloud-go/tea v1.2.1/go.mod h1:qbzof29bM/IFhLMtJPrgTGK3eauV5J2wSyEUo4OEmnA=
|
||||
github.com/alibabacloud-go/tea v1.2.2 h1:aTsR6Rl3ANWPfqeQugPglfurloyBJY85eFy7Gc1+8oU=
|
||||
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
|
||||
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
|
||||
github.com/alibabacloud-go/tea-utils v1.4.4 h1:lxCDvNCdTo9FaXKKq45+4vGETQUKNOW/qKTcX9Sk53o=
|
||||
github.com/alibabacloud-go/tea-utils v1.4.4/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.3/go.mod h1:sj1PbjPodAVTqGTA3olprfeeqqmwD0A5OQz94o9EuXQ=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0=
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.18/go.mod h1:v8ESoHo4SyHmuB4b1tJqDHxfTGEciD+yhvOU/5s1Rfk=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704 h1:PpfENOj/vPfhhy9N2OFRjpue0hjM5XqAp2thFmkXXIk=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800 h1:ie/8RxBOfKZWcrbYSJi2Z8uX8TcOlSMwPlEJh83OeOw=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU=
|
||||
github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1 h1:nJYyoFP+aqGKgPs9JeZgS1rWQ4NndNR0Zfhh161ZltU=
|
||||
github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1/go.mod h1:WzGOmFFTlUzXM03CJnHWMQ85UN6QGpOXZocCjwkiyOg=
|
||||
github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8 h1:QeUdR7JF7iNCvO/81EhxEr3wDwxk4YBoYZOq6E0AjHI=
|
||||
github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8/go.mod h1:xP0KIZry6i7oGPF24vhAPr1Q8vLZRcMcxtft5xDKwCU=
|
||||
github.com/aliyun/aliyun-secretsmanager-client-go v1.1.5 h1:8S0mtD101RDYa0LXwdoqgN0RxdMmmJYjq8g2mk7/lQ4=
|
||||
github.com/aliyun/aliyun-secretsmanager-client-go v1.1.5/go.mod h1:M19fxYz3gpm0ETnoKweYyYtqrtnVtrpKFpwsghbw+cQ=
|
||||
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
|
||||
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
|
||||
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
|
||||
github.com/aliyun/credentials-go v1.3.10/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
|
||||
github.com/aliyun/credentials-go v1.4.3 h1:N3iHyvHRMyOwY1+0qBLSf3hb5JFiOujVSVuEpgeGttY=
|
||||
github.com/aliyun/credentials-go v1.4.3/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
@@ -755,7 +814,6 @@ github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6
|
||||
github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.11-0.20170329064859-445be9e134b2/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
@@ -765,6 +823,8 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=
|
||||
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
|
||||
github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E=
|
||||
github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
@@ -813,6 +873,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ=
|
||||
github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
@@ -1162,8 +1224,9 @@ github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97Dwqy
|
||||
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
@@ -1371,6 +1434,8 @@ 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=
|
||||
@@ -1460,8 +1525,6 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/nacos-group/nacos-sdk-go v1.0.8 h1:8pEm05Cdav9sQgJSv5kyvlgfz0SzFUUGI3pWX6SiSnM=
|
||||
github.com/nacos-group/nacos-sdk-go v1.0.8/go.mod h1:hlAPn3UdzlxIlSILAyOXKxjFSvDJ9oLzTJ9hLAK1KzA=
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.1.2 h1:A8GV6j0rw80I6tTKSav/pTpEgNECYXeFvZCsiLBWGnQ=
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.1.2/go.mod h1:ys/1adWeKXXzbNWfRNbaFlX/t6HVLWdpsNDvmoWTw0g=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
|
||||
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
|
||||
@@ -1517,6 +1580,8 @@ github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJ
|
||||
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/openzipkin/zipkin-go v0.3.0/go.mod h1:4c3sLeE8xjNqehmF5RpAFLPLJxXscc0R4l6Zg0P1tTQ=
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc h1:Ak86L+yDSOzKFa7WM5bf5itSOo1e3Xh8bm5YCMUXIjQ=
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
|
||||
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||
@@ -1560,7 +1625,6 @@ github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3O
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
@@ -1593,7 +1657,6 @@ github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/prometheus/prometheus v0.45.0 h1:O/uG+Nw4kNxx/jDPxmjsSDd+9Ohql6E7ZSY1x5x/0KI=
|
||||
@@ -1643,8 +1706,9 @@ github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0=
|
||||
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
@@ -1713,6 +1777,9 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
@@ -1746,6 +1813,7 @@ github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+Seva
|
||||
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
@@ -1832,7 +1900,6 @@ go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0
|
||||
go.uber.org/automaxprocs v1.4.0/go.mod h1:/mTEdr7LvHhs0v7mjdxDreTz1OG5zdZGqgOnhWiR/+Q=
|
||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
@@ -1849,7 +1916,6 @@ go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
|
||||
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
|
||||
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
|
||||
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -1868,9 +1934,12 @@ golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
@@ -1882,8 +1951,13 @@ golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
|
||||
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
@@ -1970,6 +2044,7 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
@@ -2008,8 +2083,13 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -2059,8 +2139,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -2107,6 +2187,7 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -2156,7 +2237,6 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -2181,8 +2261,13 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
@@ -2195,8 +2280,13 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -2214,8 +2304,11 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -2225,7 +2318,6 @@ golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
@@ -2279,6 +2371,7 @@ golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjs
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
@@ -2646,6 +2739,7 @@ gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.1.1
|
||||
appVersion: 2.1.2
|
||||
description: Helm chart for deploying higress gateways
|
||||
icon: https://higress.io/img/higress_logo_small.png
|
||||
home: http://higress.io/
|
||||
@@ -15,4 +15,4 @@ dependencies:
|
||||
repository: "file://../redis"
|
||||
version: 0.0.1
|
||||
type: application
|
||||
version: 2.1.1
|
||||
version: 2.1.2
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: file://../core
|
||||
version: 2.1.1
|
||||
version: 2.1.2
|
||||
- name: higress-console
|
||||
repository: https://higress.io/helm-charts/
|
||||
version: 2.1.1
|
||||
digest: sha256:a40f56252d0aa995fbf62952a6d23304dd84845caf5766bc64f7270cb00f01e9
|
||||
generated: "2025-04-18T16:45:23.961507+08:00"
|
||||
version: 2.1.2
|
||||
digest: sha256:7612de239141ca0d27400f7d5b9a786acd98826f511e2e3ed65ccd9d2c9f1700
|
||||
generated: "2025-04-29T20:52:39.996652+08:00"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.1.1
|
||||
appVersion: 2.1.2
|
||||
description: Helm chart for deploying Higress gateways
|
||||
icon: https://higress.io/img/higress_logo_small.png
|
||||
home: http://higress.io/
|
||||
@@ -12,9 +12,9 @@ sources:
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: "file://../core"
|
||||
version: 2.1.1
|
||||
version: 2.1.2
|
||||
- name: higress-console
|
||||
repository: "https://higress.io/helm-charts/"
|
||||
version: 2.1.1
|
||||
version: 2.1.2
|
||||
type: application
|
||||
version: 2.1.1
|
||||
version: 2.1.2
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## Higress for Kubernetes
|
||||
|
||||
Higress 是基于阿里巴巴内部网关实践构建的云原生 API 网关。
|
||||
Higress 是基于阿里巴巴内部网关实践的云原生 API 网关。
|
||||
|
||||
依托 Istio 和 Envoy,Higress 实现了流量网关、微服务网关和安全网关三重架构的融合,从而大幅降低了部署、运维成本。
|
||||
通过基于 Istio 和 Envoy,Higress 实现了流量网关、微服务网关和安全网关的三重网关架构的集成,从而大大降低了部署、运维的成本。
|
||||
|
||||
## 设置仓库信息
|
||||
|
||||
@@ -13,7 +13,7 @@ helm repo update
|
||||
|
||||
## 安装
|
||||
|
||||
以 `higress` 为发布名称安装 chart:
|
||||
使用名为 `higress` 的版本来安装 chart:
|
||||
|
||||
```console
|
||||
helm install higress -n higress-system higress.io/higress --create-namespace --render-subchart-notes
|
||||
@@ -21,168 +21,60 @@ helm install higress -n higress-system higress.io/higress --create-namespace --r
|
||||
|
||||
## 卸载
|
||||
|
||||
要卸载/删除 higress 部署:
|
||||
卸载删除 higress 部署:
|
||||
|
||||
```console
|
||||
helm delete higress -n higress-system
|
||||
```
|
||||
|
||||
该命令会移除与 chart 相关的所有 Kubernetes 组件,并删除发布。
|
||||
该命令会删除与 chart 相关的所有 Kubernetes 组件并删除发行版。
|
||||
|
||||
## 参数
|
||||
|
||||
## 值
|
||||
## 配置值
|
||||
|
||||
| 键 | 类型 | 默认值 | 描述 |
|
||||
|-----|------|---------|-------------|
|
||||
| clusterName | 字符串 | `""` | |
|
||||
| controller.affinity | 对象 | `{}` | |
|
||||
| controller.automaticHttps.email | 字符串 | `""` | |
|
||||
| controller.automaticHttps.enabled | 布尔值 | `true` | |
|
||||
| controller.autoscaling.enabled | 布尔值 | `false` | |
|
||||
| controller.autoscaling.maxReplicas | 整数 | `5` | |
|
||||
| controller.autoscaling.minReplicas | 整数 | `1` | |
|
||||
| controller.autoscaling.targetCPUUtilizationPercentage | 整数 | `80` | |
|
||||
| controller.env | 对象 | `{}` | |
|
||||
| controller.hub | 字符串 | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
|
||||
| controller.image | 字符串 | `"higress"` | |
|
||||
| controller.imagePullSecrets | 列表 | `[]` | |
|
||||
| controller.labels | 对象 | `{}` | |
|
||||
| controller.name | 字符串 | `"higress-controller"` | |
|
||||
| controller.nodeSelector | 对象 | `{}` | |
|
||||
| controller.podAnnotations | 对象 | `{}` | |
|
||||
| controller.podSecurityContext | 对象 | `{}` | |
|
||||
| controller.ports[0].name | 字符串 | `"http"` | |
|
||||
| controller.ports[0].port | 整数 | `8888` | |
|
||||
| controller.ports[0].protocol | 字符串 | `"TCP"` | |
|
||||
| controller.ports[0].targetPort | 整数 | `8888` | |
|
||||
| controller.ports[1].name | 字符串 | `"http-solver"` | |
|
||||
| controller.ports[1].port | 整数 | `8889` | |
|
||||
| controller.ports[1].protocol | 字符串 | `"TCP"` | |
|
||||
| controller.ports[1].targetPort | 整数 | `8889` | |
|
||||
| controller.ports[2].name | 字符串 | `"grpc"` | |
|
||||
| controller.ports[2].port | 整数 | `15051` | |
|
||||
| controller.ports[2].protocol | 字符串 | `"TCP"` | |
|
||||
| controller.ports[2].targetPort | 整数 | `15051` | |
|
||||
| controller.probe.httpGet.path | 字符串 | `"/ready"` | |
|
||||
| controller.probe.httpGet.port | 整数 | `8888` | |
|
||||
| controller.probe.initialDelaySeconds | 整数 | `1` | |
|
||||
| controller.probe.periodSeconds | 整数 | `3` | |
|
||||
| controller.probe.timeoutSeconds | 整数 | `5` | |
|
||||
| controller.rbac.create | 布尔值 | `true` | |
|
||||
| controller.replicas | 整数 | `1` | Higress Controller 的 Pod 数量 |
|
||||
| controller.resources.limits.cpu | 字符串 | `"1000m"` | |
|
||||
| controller.resources.limits.memory | 字符串 | `"2048Mi"` | |
|
||||
| controller.resources.requests.cpu | 字符串 | `"500m"` | |
|
||||
| controller.resources.requests.memory | 字符串 | `"2048Mi"` | |
|
||||
| controller.securityContext | 对象 | `{}` | |
|
||||
| controller.service.type | 字符串 | `"ClusterIP"` | |
|
||||
| controller.serviceAccount.annotations | 对象 | `{}` | 添加到服务账户的注解 |
|
||||
| controller.serviceAccount.create | 布尔值 | `true` | 指定是否创建服务账户 |
|
||||
| controller.serviceAccount.name | 字符串 | `""` | 如果未设置且 create 为 true,则使用 fullname 模板生成名称 |
|
||||
| controller.tag | 字符串 | `""` | |
|
||||
| controller.tolerations | 列表 | `[]` | |
|
||||
| downstream | 对象 | `{"connectionBufferLimits":32768,"http2":{"initialConnectionWindowSize":1048576,"initialStreamWindowSize":65535,"maxConcurrentStreams":100},"idleTimeout":180,"maxRequestHeadersKb":60,"routeTimeout":0}` | 下游配置设置 |
|
||||
| gateway.affinity | 对象 | `{}` | |
|
||||
| gateway.annotations | 对象 | `{}` | 应用到所有资源的注解 |
|
||||
| gateway.autoscaling.enabled | 布尔值 | `false` | |
|
||||
| gateway.autoscaling.maxReplicas | 整数 | `5` | |
|
||||
| gateway.autoscaling.minReplicas | 整数 | `1` | |
|
||||
| gateway.autoscaling.targetCPUUtilizationPercentage | 整数 | `80` | |
|
||||
| gateway.containerSecurityContext | 字符串 | `nil` | |
|
||||
| gateway.env | 对象 | `{}` | Pod 环境变量 |
|
||||
| gateway.hostNetwork | 布尔值 | `false` | |
|
||||
| gateway.httpPort | 整数 | `80` | |
|
||||
| gateway.httpsPort | 整数 | `443` | |
|
||||
| gateway.hub | 字符串 | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
|
||||
| gateway.image | 字符串 | `"gateway"` | |
|
||||
| gateway.kind | 字符串 | `"Deployment"` | 使用 `DaemonSet` 或 `Deployment` |
|
||||
| gateway.labels | 对象 | `{}` | 应用到所有资源的标签 |
|
||||
| gateway.metrics.enabled | 布尔值 | `false` | 如果为 true,则为网关创建 PodMonitor 或 VMPodScrape |
|
||||
| gateway.metrics.honorLabels | 布尔值 | `false` | |
|
||||
| gateway.metrics.interval | 字符串 | `""` | |
|
||||
| gateway.metrics.metricRelabelConfigs | 列表 | `[]` | 用于 operator.victoriametrics.com/v1beta1.VMPodScrape |
|
||||
| gateway.metrics.metricRelabelings | 列表 | `[]` | 用于 monitoring.coreos.com/v1.PodMonitor |
|
||||
| gateway.metrics.provider | 字符串 | `"monitoring.coreos.com"` | CustomResourceDefinition 的提供者组名,可以是 monitoring.coreos.com 或 operator.victoriametrics.com |
|
||||
| gateway.metrics.rawSpec | 对象 | `{}` | 更多原始的 podMetricsEndpoints 规范 |
|
||||
| gateway.metrics.relabelConfigs | 列表 | `[]` | |
|
||||
| gateway.metrics.relabelings | 列表 | `[]` | |
|
||||
| gateway.metrics.scrapeTimeout | 字符串 | `""` | |
|
||||
| gateway.name | 字符串 | `"higress-gateway"` | |
|
||||
| gateway.networkGateway | 字符串 | `""` | 如果指定,网关将作为给定网络的网络网关。 |
|
||||
| gateway.nodeSelector | 对象 | `{}` | |
|
||||
| gateway.podAnnotations."prometheus.io/path" | 字符串 | `"/stats/prometheus"` | |
|
||||
| gateway.podAnnotations."prometheus.io/port" | 字符串 | `"15020"` | |
|
||||
| gateway.podAnnotations."prometheus.io/scrape" | 字符串 | `"true"` | |
|
||||
| gateway.podAnnotations."sidecar.istio.io/inject" | 字符串 | `"false"` | |
|
||||
| gateway.rbac.enabled | 布尔值 | `true` | 如果启用,将创建角色以启用从网关访问证书。当使用 http://gateway-api.org/ 时不需要。 |
|
||||
| gateway.readinessFailureThreshold | 整数 | `30` | 指示准备失败前的连续失败探测次数。 |
|
||||
| gateway.readinessInitialDelaySeconds | 整数 | `1` | 准备探测的初始延迟秒数。 |
|
||||
| gateway.readinessPeriodSeconds | 整数 | `2` | 准备探测之间的间隔。 |
|
||||
| gateway.readinessSuccessThreshold | 整数 | `1` | 指示准备成功前的连续成功探测次数。 |
|
||||
| gateway.readinessTimeoutSeconds | 整数 | `3` | 准备探测的超时秒数 |
|
||||
| gateway.replicas | 整数 | `2` | Higress Gateway 的 Pod 数量 |
|
||||
| gateway.resources.limits.cpu | 字符串 | `"2000m"` | |
|
||||
| gateway.resources.limits.memory | 字符串 | `"2048Mi"` | |
|
||||
| gateway.resources.requests.cpu | 字符串 | `"2000m"` | |
|
||||
| gateway.resources.requests.memory | 字符串 | `"2048Mi"` | |
|
||||
| gateway.revision | 字符串 | `""` | 修订声明此网关属于哪个修订 |
|
||||
| gateway.rollingMaxSurge | 字符串 | `"100%"` | |
|
||||
| gateway.rollingMaxUnavailable | 字符串 | `"25%"` | |
|
||||
| gateway.securityContext | 字符串 | `nil` | 定义 Pod 的安全上下文。如果未设置,将自动设置为绑定到端口 80 和 443 所需的最小权限。在 Kubernetes 1.22+ 上,这只需要 `net.ipv4.ip_unprivileged_port_start` 系统调用。 |
|
||||
| gateway.service.annotations | 对象 | `{}` | |
|
||||
| gateway.service.externalTrafficPolicy | 字符串 | `""` | |
|
||||
| gateway.service.loadBalancerClass | 字符串 | `""` | |
|
||||
| gateway.service.loadBalancerIP | 字符串 | `""` | |
|
||||
| gateway.service.loadBalancerSourceRanges | 列表 | `[]` | |
|
||||
| gateway.service.ports[0].name | 字符串 | `"http2"` | |
|
||||
| gateway.service.ports[0].port | 整数 | `80` | |
|
||||
| gateway.service.ports[0].protocol | 字符串 | `"TCP"` | |
|
||||
| gateway.service.ports[0].targetPort | 整数 | `80` | |
|
||||
| gateway.service.ports[1].name | 字符串 | `"https"` | |
|
||||
| gateway.service.ports[1].port | 整数 | `443` | |
|
||||
| gateway.service.ports[1].protocol | 字符串 | `"TCP"` | |
|
||||
| gateway.service.ports[1].targetPort | 整数 | `443` | |
|
||||
| gateway.service.type | 字符串 | `"LoadBalancer"` | 服务类型。设置为 "None" 以完全禁用服务 |
|
||||
| gateway.serviceAccount.annotations | 对象 | `{}` | 添加到服务账户的注解 |
|
||||
| gateway.serviceAccount.create | 布尔值 | `true` | 如果设置,将创建服务账户。否则,使用默认值 |
|
||||
| gateway.serviceAccount.name | 字符串 | `""` | 要使用的服务账户名称。如果未设置,则使用发布名称 |
|
||||
| gateway.tag | 字符串 | `""` | |
|
||||
| gateway.tolerations | 列表 | `[]` | |
|
||||
| gateway.unprivilegedPortSupported | 字符串 | `nil` | |
|
||||
| global.autoscalingv2API | 布尔值 | `true` | 是否使用 autoscaling/v2 模板进行 HPA 设置,仅供内部使用,用户不应配置。 |
|
||||
| global.caAddress | 字符串 | `""` | 自定义的 CA 地址,用于为集群中的 Pod 检索证书。CSR 客户端(如 Istio Agent 和 ingress gateways)可以使用此地址指定 CA 端点。如果未明确设置,则默认为 Istio 发现地址。 |
|
||||
| global.caName | 字符串 | `""` | 工作负载证书的 CA 名称。例如,当 caName=GkeWorkloadCertificate 时,GKE 工作负载证书将用作工作负载的证书。默认值为 "",当 caName="" 时,CA 将通过其他机制(如环境变量 CA_PROVIDER)配置。 |
|
||||
| global.configCluster | 布尔值 | `false` | 将远程集群配置为外部 istiod 的配置集群。 |
|
||||
| global.defaultPodDisruptionBudget | 对象 | `{"enabled":false}` | 为控制平面启用 Pod 中断预算,用于确保 Istio 控制平面组件逐步升级或恢复。 |
|
||||
| global.defaultResources | 对象 | `{"requests":{"cpu":"10m"}}` | 应用于所有部署的最小请求资源集,以便 Horizontal Pod Autoscaler 能够正常工作(如果设置)。每个组件可以通过在相关部分添加自己的资源块并设置所需的资源值来覆盖这些默认值。 |
|
||||
| global.defaultUpstreamConcurrencyThreshold | 整数 | `10000` | |
|
||||
| global.disableAlpnH2 | 布尔值 | `false` | 是否在 ALPN 中禁用 HTTP/2 |
|
||||
| global.enableGatewayAPI | 布尔值 | `false` | 如果为 true,Higress Controller 还将监控 Gateway API 资源 |
|
||||
| global.enableH3 | 布尔值 | `false` | |
|
||||
| global.enableIPv6 | 布尔值 | `false` | |
|
||||
| global.enableIstioAPI | 布尔值 | `true` | 如果为 true,Higress Controller 还将监控 istio 资源 |
|
||||
| global.enableLDSCache | 布尔值 | `true` | |
|
||||
| global.enableProxyProtocol | 布尔值 | `false` | |
|
||||
| global.enablePushAllMCPClusters | 布尔值 | `true` | |
|
||||
| global.enableSRDS | 布尔值 | `true` | |
|
||||
| global.enableStatus | 布尔值 | `true` | 如果为 true,Higress 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、tracing),ImagePullSecrets 将添加到相应的 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 | `""` | |
|
||||
| controller.automaticHttps.enabled | bool | `true` | |
|
||||
| controller.autoscaling.enabled | bool | `false` | |
|
||||
| controller.autoscaling.maxReplicas | int | `5` | |
|
||||
| controller.autoscaling.minReplicas | int | `1` | |
|
||||
| controller.autoscaling.targetCPUUtilizationPercentage | int | `80` | |
|
||||
| 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 | `{}` | |
|
||||
| controller.podLabels | object | `{}` | 应用到 pod 上的标签 |
|
||||
| controller.podSecurityContext | object | `{}` | |
|
||||
| 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.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` | |
|
||||
| controller.replicas | int | `1` | Higress Controller pods 的数量 |
|
||||
| controller.resources.limits.cpu | string | `"1000m"` | |
|
||||
| controller.resources.limits.memory | string | `"2048Mi"` | |
|
||||
| controller.resources.requests.cpu | string | `"500m"` | |
|
||||
| controller.resources.requests.memory | string | `"2048Mi"` | |
|
||||
| gateway.metrics.enabled | bool | `false` | 如果为 true,则为gateway创建PodMonitor或VMPodScrape |
|
||||
| gateway.metrics.provider | string | `monitoring.coreos.com` | CustomResourceDefinition 的提供商组名,可以是 monitoring.coreos.com 或 operator.victoriametrics.com |
|
||||
| gateway.readinessFailureThreshold | int | `30` | 成功进行探针测试前连续失败探针的最大次数。 |
|
||||
| global MeshNetworks | object | `{}` | |
|
||||
| global.tracer.datadog.address | string | `"$(HOST_IP):8126"` | 提交给 Datadog agent 的 Host:Port 。|
|
||||
| redis.redis.persistence.enabled | bool | `false` | 启用 Redis 持久性,默认为 false |
|
||||
| redis.redis.persistence.size | string | `"1Gi"` | Persistent Volume 大小 |
|
||||
| redis.redis.service.port | int | `6379` | Exporter service 端口 |
|
||||
| tracing.skywalking.port | int | `11800` | |
|
||||
| upstream.connectionBufferLimits | int | `10485760` | 上游连接缓冲限制(字节)|
|
||||
|
||||
12
hgctl/go.mod
12
hgctl/go.mod
@@ -242,15 +242,15 @@ require (
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/oauth2 v0.13.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/term v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/term v0.27.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
|
||||
18
hgctl/go.sum
18
hgctl/go.sum
@@ -1789,8 +1789,9 @@ golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
|
||||
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
@@ -1909,8 +1910,9 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1961,8 +1963,9 @@ golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -2069,8 +2072,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
@@ -2086,8 +2090,9 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -2108,8 +2113,9 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
||||
Submodule istio/istio updated: e6578f7dd0...1492505b14
Submodule istio/proxy updated: 862975858e...d411a4f019
@@ -22,5 +22,6 @@ var (
|
||||
GatewayName = env.RegisterStringVar("GATEWAY_NAME", "higress-gateway", "").Get()
|
||||
// Revision is the value of the Istio control plane revision, e.g. "canary",
|
||||
// and is the value used by the "istio.io/rev" label.
|
||||
Revision = env.Register("REVISION", "", "").Get()
|
||||
Revision = env.Register("REVISION", "", "").Get()
|
||||
McpServerWasmImageUrl = env.RegisterStringVar("MCP_SERVER_WASM_IMAGE_URL", "oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/mcp-server/all-in-one:1.0.0", "").Get()
|
||||
)
|
||||
|
||||
@@ -152,6 +152,7 @@ type IngressConfig struct {
|
||||
|
||||
httpsConfigMgr *cert.ConfigMgr
|
||||
|
||||
commonOptions common.Options
|
||||
// templateProcessor processes template variables in config
|
||||
templateProcessor *TemplateProcessor
|
||||
|
||||
@@ -197,6 +198,7 @@ func NewIngressConfig(localKubeClient kube.Client, xdsUpdater istiomodel.XDSUpda
|
||||
namespace: namespace,
|
||||
wasmPlugins: make(map[string]*extensions.WasmPlugin),
|
||||
http2rpcs: make(map[string]*higressv1.Http2Rpc),
|
||||
commonOptions: options,
|
||||
}
|
||||
|
||||
// Initialize secret config manager
|
||||
@@ -588,6 +590,13 @@ func (m *IngressConfig) convertVirtualService(configs []common.WrapperConfig) []
|
||||
Spec: vs,
|
||||
})
|
||||
}
|
||||
// add vs from naco3 for mcp server
|
||||
if m.RegistryReconciler != nil {
|
||||
allConfigsFromMcp := m.RegistryReconciler.GetAllConfigs(gvk.VirtualService)
|
||||
for _, cfg := range allConfigsFromMcp {
|
||||
out = append(out, *cfg)
|
||||
}
|
||||
}
|
||||
|
||||
// We generate some specific envoy filter here to avoid duplicated computation.
|
||||
m.convertEnvoyFilter(&convertOptions)
|
||||
@@ -674,6 +683,13 @@ func (m *IngressConfig) convertWasmPlugin([]common.WrapperConfig) []config.Confi
|
||||
Spec: wasmPlugin,
|
||||
})
|
||||
}
|
||||
// add wasm plugin from nacos for mcp server
|
||||
if m.RegistryReconciler != nil {
|
||||
wasmFromMcp := m.RegistryReconciler.GetAllConfigs(gvk.WasmPlugin)
|
||||
for _, cfg := range wasmFromMcp {
|
||||
out = append(out, *cfg)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -684,6 +700,7 @@ func (m *IngressConfig) convertServiceEntry([]common.WrapperConfig) []config.Con
|
||||
serviceEntries := m.RegistryReconciler.GetAllServiceWrapper()
|
||||
IngressLog.Infof("Found mcp serviceEntries %v", serviceEntries)
|
||||
out := make([]config.Config, 0, len(serviceEntries))
|
||||
hostSets := sets.Set[string]{}
|
||||
for _, se := range serviceEntries {
|
||||
out = append(out, config.Config{
|
||||
Meta: config.Meta{
|
||||
@@ -698,6 +715,15 @@ func (m *IngressConfig) convertServiceEntry([]common.WrapperConfig) []config.Con
|
||||
},
|
||||
Spec: se.ServiceEntry,
|
||||
})
|
||||
hostSets.Insert(se.ServiceEntry.Hosts[0])
|
||||
}
|
||||
// add service entry by host from nacos3 for mcp server
|
||||
seFromMcp := m.RegistryReconciler.GetAllConfigs(gvk.ServiceEntry)
|
||||
for _, cfg := range seFromMcp {
|
||||
se := cfg.Spec.(*networking.ServiceEntry)
|
||||
if !hostSets.Contains(se.Hosts[0]) {
|
||||
out = append(out, *cfg)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -768,6 +794,10 @@ 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 {
|
||||
@@ -904,7 +934,7 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
result := &extensions.WasmPlugin{
|
||||
Selector: &istiotype.WorkloadSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"higress": m.namespace + "-higress-gateway",
|
||||
m.commonOptions.GatewaySelectorKey: m.commonOptions.GatewaySelectorValue,
|
||||
},
|
||||
},
|
||||
Url: obj.Url,
|
||||
@@ -1135,6 +1165,28 @@ func (m *IngressConfig) AddOrUpdateMcpBridge(clusterNamespacedName util.ClusterN
|
||||
// Set this label so that we do not compare configs and just push.
|
||||
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
|
||||
}
|
||||
vsMetadata := config.Meta{
|
||||
Name: "mcpbridge-virtualservice",
|
||||
Namespace: m.namespace,
|
||||
GroupVersionKind: gvk.VirtualService,
|
||||
// Set this label so that we do not compare configs and just push.
|
||||
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
|
||||
}
|
||||
wasmMetadata := config.Meta{
|
||||
Name: "mcpbridge-wasmplugin",
|
||||
Namespace: m.namespace,
|
||||
GroupVersionKind: gvk.WasmPlugin,
|
||||
// Set this label so that we do not compare configs and just push.
|
||||
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
|
||||
}
|
||||
efMetadata := config.Meta{
|
||||
Name: "mcpbridge-envoyfilter",
|
||||
Namespace: m.namespace,
|
||||
GroupVersionKind: gvk.EnvoyFilter,
|
||||
// Set this label so that we do not compare configs and just push.
|
||||
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
|
||||
}
|
||||
|
||||
for _, f := range m.serviceEntryHandlers {
|
||||
IngressLog.Debug("McpBridge triggerd serviceEntry update")
|
||||
f(config.Config{Meta: seMetadata}, config.Config{Meta: seMetadata}, istiomodel.EventUpdate)
|
||||
@@ -1143,9 +1195,22 @@ func (m *IngressConfig) AddOrUpdateMcpBridge(clusterNamespacedName util.ClusterN
|
||||
IngressLog.Debug("McpBridge triggerd destinationRule update")
|
||||
f(config.Config{Meta: drMetadata}, config.Config{Meta: drMetadata}, istiomodel.EventUpdate)
|
||||
}
|
||||
}, m.localKubeClient, m.namespace)
|
||||
for _, f := range m.virtualServiceHandlers {
|
||||
IngressLog.Debug("McpBridge triggerd virtualservice update")
|
||||
f(config.Config{Meta: vsMetadata}, config.Config{Meta: vsMetadata}, istiomodel.EventUpdate)
|
||||
}
|
||||
for _, f := range m.wasmPluginHandlers {
|
||||
IngressLog.Debug("McpBridge triggerd wasmplugin update")
|
||||
f(config.Config{Meta: wasmMetadata}, config.Config{Meta: wasmMetadata}, istiomodel.EventUpdate)
|
||||
}
|
||||
for _, f := range m.envoyFilterHandlers {
|
||||
IngressLog.Debug("McpBridge triggerd envoyfilter update")
|
||||
f(config.Config{Meta: efMetadata}, config.Config{Meta: efMetadata}, istiomodel.EventUpdate)
|
||||
}
|
||||
}, m.localKubeClient, m.namespace, m.clusterId.String())
|
||||
}
|
||||
reconciler := m.RegistryReconciler
|
||||
m.configmapMgr.SetMcpReconciler(m.RegistryReconciler)
|
||||
err = reconciler.Reconcile(mcpbridge)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Mcpbridge reconcile failed, err:%v", err)
|
||||
|
||||
97
pkg/ingress/kube/common/model_test.go
Normal file
97
pkg/ingress/kube/common/model_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pkg/config"
|
||||
)
|
||||
|
||||
func TestIngressDomainCache(t *testing.T) {
|
||||
cache := NewIngressDomainCache()
|
||||
assert.NotNil(t, cache)
|
||||
assert.NotNil(t, cache.Valid)
|
||||
assert.Empty(t, cache.Invalid)
|
||||
|
||||
cache.Valid["example.com"] = &IngressDomainBuilder{
|
||||
Host: "example.com",
|
||||
Protocol: HTTP,
|
||||
ClusterId: "cluster-1",
|
||||
Ingress: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "test-ingress",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cache.Invalid = append(cache.Invalid, model.IngressDomain{
|
||||
Host: "invalid.com",
|
||||
Error: "invalid domain",
|
||||
})
|
||||
|
||||
result := cache.Extract()
|
||||
assert.Equal(t, 1, len(result.Valid))
|
||||
assert.Equal(t, "example.com", result.Valid[0].Host)
|
||||
assert.Equal(t, string(HTTP), result.Valid[0].Protocol)
|
||||
|
||||
assert.Equal(t, 1, len(result.Invalid))
|
||||
assert.Equal(t, "invalid.com", result.Invalid[0].Host)
|
||||
}
|
||||
|
||||
func TestIngressDomainBuilder(t *testing.T) {
|
||||
builder := &IngressDomainBuilder{
|
||||
Host: "example.com",
|
||||
Protocol: HTTP,
|
||||
ClusterId: "cluster-1",
|
||||
Ingress: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "test-ingress",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
domain := builder.Build()
|
||||
assert.Equal(t, "example.com", domain.Host)
|
||||
assert.Equal(t, string(HTTP), domain.Protocol)
|
||||
|
||||
builder.Event = MissingSecret
|
||||
eventDomain := builder.Build()
|
||||
assert.Contains(t, eventDomain.Error, "misses secret")
|
||||
|
||||
builder.Event = DuplicatedTls
|
||||
builder.PreIngress = &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "pre-ingress",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
builder.PreIngress.Meta.Annotations = map[string]string{
|
||||
ClusterIdAnnotation: "pre-cluster",
|
||||
}
|
||||
dupDomain := builder.Build()
|
||||
assert.Contains(t, dupDomain.Error, "conflicted with ingress")
|
||||
|
||||
builder.Protocol = HTTPS
|
||||
builder.SecretName = "test-secret"
|
||||
builder.Event = ""
|
||||
httpsDomain := builder.Build()
|
||||
assert.Equal(t, string(HTTPS), httpsDomain.Protocol)
|
||||
assert.Equal(t, "test-secret", httpsDomain.SecretName)
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"testing"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pkg/config"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -556,3 +557,514 @@ func TestSortHTTPRoutesWithMoreRules(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBackendResource(t *testing.T) {
|
||||
groupStr := "networking.higress.io"
|
||||
testCases := []struct {
|
||||
name string
|
||||
resource *v1.TypedLocalObjectReference
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "nil resource",
|
||||
resource: nil,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "nil APIGroup",
|
||||
resource: &v1.TypedLocalObjectReference{
|
||||
APIGroup: nil,
|
||||
Kind: "McpBridge",
|
||||
Name: "default",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "wrong APIGroup",
|
||||
resource: &v1.TypedLocalObjectReference{
|
||||
APIGroup: &groupStr,
|
||||
Kind: "McpBridge",
|
||||
Name: "wrong-name",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "wrong Kind",
|
||||
resource: &v1.TypedLocalObjectReference{
|
||||
APIGroup: &groupStr,
|
||||
Kind: "WrongKind",
|
||||
Name: "default",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "valid resource",
|
||||
resource: &v1.TypedLocalObjectReference{
|
||||
APIGroup: &groupStr,
|
||||
Kind: "McpBridge",
|
||||
Name: "default",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := ValidateBackendResource(tc.resource)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateAnnotations(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
annotations map[string]string
|
||||
options Options
|
||||
expected map[string]string
|
||||
}{
|
||||
{
|
||||
name: "empty annotations",
|
||||
annotations: map[string]string{},
|
||||
options: Options{
|
||||
ClusterId: "test-cluster",
|
||||
RawClusterId: "raw-test-cluster",
|
||||
},
|
||||
expected: map[string]string{
|
||||
ClusterIdAnnotation: "test-cluster",
|
||||
RawClusterIdAnnotation: "raw-test-cluster",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "existing annotations",
|
||||
annotations: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
options: Options{
|
||||
ClusterId: "test-cluster",
|
||||
RawClusterId: "raw-test-cluster",
|
||||
},
|
||||
expected: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
ClusterIdAnnotation: "test-cluster",
|
||||
RawClusterIdAnnotation: "raw-test-cluster",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "overwrite existing cluster annotations",
|
||||
annotations: map[string]string{
|
||||
ClusterIdAnnotation: "old-cluster",
|
||||
RawClusterIdAnnotation: "old-raw-cluster",
|
||||
"key1": "value1",
|
||||
},
|
||||
options: Options{
|
||||
ClusterId: "new-cluster",
|
||||
RawClusterId: "new-raw-cluster",
|
||||
},
|
||||
expected: map[string]string{
|
||||
ClusterIdAnnotation: "new-cluster",
|
||||
RawClusterIdAnnotation: "new-raw-cluster",
|
||||
"key1": "value1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := CreateOrUpdateAnnotations(tc.annotations, tc.options)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClusterId(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
annotations map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "nil annotations",
|
||||
annotations: nil,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "empty annotations",
|
||||
annotations: map[string]string{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "with cluster id",
|
||||
annotations: map[string]string{
|
||||
ClusterIdAnnotation: "test-cluster",
|
||||
},
|
||||
expected: "test-cluster",
|
||||
},
|
||||
{
|
||||
name: "with other annotations",
|
||||
annotations: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := GetClusterId(tc.annotations)
|
||||
assert.Equal(t, tc.expected, string(result))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToDNSLabelValidAndCleanHost(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{
|
||||
name: "simple host",
|
||||
input: "example.com",
|
||||
},
|
||||
{
|
||||
name: "wildcard host",
|
||||
input: "*.example.com",
|
||||
},
|
||||
{
|
||||
name: "long host",
|
||||
input: "very-long-subdomain.example-service.my-namespace.svc.cluster.local",
|
||||
},
|
||||
{
|
||||
name: "empty host",
|
||||
input: "",
|
||||
},
|
||||
{
|
||||
name: "ip address",
|
||||
input: "192.168.1.1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Test internal convertToDNSLabelValid function (through CleanHost)
|
||||
result := CleanHost(tc.input)
|
||||
|
||||
// Validate result
|
||||
assert.NotEmpty(t, result)
|
||||
assert.Equal(t, 16, len(result)) // MD5 hash format is fixed length of 16 bytes
|
||||
|
||||
// Consistency check - same input should produce same output
|
||||
result2 := CleanHost(tc.input)
|
||||
assert.Equal(t, result, result2)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitServiceFQDN(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
fqdn string
|
||||
expectedSvc string
|
||||
expectedNs string
|
||||
expectedValid bool
|
||||
}{
|
||||
{
|
||||
name: "simple fqdn",
|
||||
fqdn: "service.namespace",
|
||||
expectedSvc: "service",
|
||||
expectedNs: "namespace",
|
||||
expectedValid: true,
|
||||
},
|
||||
{
|
||||
name: "full k8s fqdn",
|
||||
fqdn: "service.namespace.svc.cluster.local",
|
||||
expectedSvc: "service",
|
||||
expectedNs: "namespace",
|
||||
expectedValid: true,
|
||||
},
|
||||
{
|
||||
name: "just service name",
|
||||
fqdn: "service",
|
||||
expectedSvc: "",
|
||||
expectedNs: "",
|
||||
expectedValid: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
fqdn: "",
|
||||
expectedSvc: "",
|
||||
expectedNs: "",
|
||||
expectedValid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
svc, ns, valid := SplitServiceFQDN(tc.fqdn)
|
||||
assert.Equal(t, tc.expectedSvc, svc)
|
||||
assert.Equal(t, tc.expectedNs, ns)
|
||||
assert.Equal(t, tc.expectedValid, valid)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertBackendService(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
dest *networking.HTTPRouteDestination
|
||||
expected model.BackendService
|
||||
}{
|
||||
{
|
||||
name: "simple service",
|
||||
dest: &networking.HTTPRouteDestination{
|
||||
Destination: &networking.Destination{
|
||||
Host: "service.namespace",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
Weight: 100,
|
||||
},
|
||||
expected: model.BackendService{
|
||||
Name: "service",
|
||||
Namespace: "namespace",
|
||||
Port: 80,
|
||||
Weight: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "full k8s FQDN",
|
||||
dest: &networking.HTTPRouteDestination{
|
||||
Destination: &networking.Destination{
|
||||
Host: "service.namespace.svc.cluster.local",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 8080,
|
||||
},
|
||||
},
|
||||
Weight: 50,
|
||||
},
|
||||
expected: model.BackendService{
|
||||
Name: "service",
|
||||
Namespace: "namespace",
|
||||
Port: 8080,
|
||||
Weight: 50,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := ConvertBackendService(tc.dest)
|
||||
assert.Equal(t, tc.expected.Name, result.Name)
|
||||
assert.Equal(t, tc.expected.Namespace, result.Namespace)
|
||||
assert.Equal(t, tc.expected.Port, result.Port)
|
||||
assert.Equal(t, tc.expected.Weight, result.Weight)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateConvertedName(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
items []string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "empty slice",
|
||||
items: []string{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "single item",
|
||||
items: []string{"example"},
|
||||
expected: "example",
|
||||
},
|
||||
{
|
||||
name: "multiple items",
|
||||
items: []string{"part1", "part2", "part3"},
|
||||
expected: "part1-part2-part3",
|
||||
},
|
||||
{
|
||||
name: "with empty strings",
|
||||
items: []string{"part1", "", "part3"},
|
||||
expected: "part1-part3",
|
||||
},
|
||||
{
|
||||
name: "all empty strings",
|
||||
items: []string{"", "", ""},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := CreateConvertedName(tc.items...)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortIngressByCreationTime(t *testing.T) {
|
||||
configs := []config.Config{
|
||||
{
|
||||
Meta: config.Meta{
|
||||
Name: "c-ingress",
|
||||
Namespace: "ns1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Meta: config.Meta{
|
||||
Name: "a-ingress",
|
||||
Namespace: "ns1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Meta: config.Meta{
|
||||
Name: "b-ingress",
|
||||
Namespace: "ns1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expected := []string{"a-ingress", "b-ingress", "c-ingress"}
|
||||
|
||||
SortIngressByCreationTime(configs)
|
||||
|
||||
var actual []string
|
||||
for _, cfg := range configs {
|
||||
actual = append(actual, cfg.Name)
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, actual, "When the timestamps are the same, the configuration should be sorted by name")
|
||||
|
||||
sameNamespaceConfigs := []config.Config{
|
||||
{
|
||||
Meta: config.Meta{
|
||||
Name: "same-name",
|
||||
Namespace: "c-ns",
|
||||
},
|
||||
},
|
||||
{
|
||||
Meta: config.Meta{
|
||||
Name: "same-name",
|
||||
Namespace: "a-ns",
|
||||
},
|
||||
},
|
||||
{
|
||||
Meta: config.Meta{
|
||||
Name: "same-name",
|
||||
Namespace: "b-ns",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedNamespace := []string{"a-ns", "b-ns", "c-ns"}
|
||||
|
||||
SortIngressByCreationTime(sameNamespaceConfigs)
|
||||
|
||||
var actualNamespace []string
|
||||
for _, cfg := range sameNamespaceConfigs {
|
||||
actualNamespace = append(actualNamespace, cfg.Namespace)
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedNamespace, actualNamespace, "When the names are the same, the configuration should be sorted by namespace")
|
||||
}
|
||||
|
||||
func TestPartMd5(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
length int
|
||||
}{
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
length: 8,
|
||||
},
|
||||
{
|
||||
name: "simple string",
|
||||
input: "test",
|
||||
length: 8,
|
||||
},
|
||||
{
|
||||
name: "complex string",
|
||||
input: "this-is-a-long-string-with-special-chars-!@#$%^&*()",
|
||||
length: 8,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := partMd5(tc.input)
|
||||
|
||||
// Check result format
|
||||
assert.Equal(t, tc.length, len(result), "MD5 hash excerpt should be 8 characters")
|
||||
|
||||
// Run twice to ensure deterministic output
|
||||
result2 := partMd5(tc.input)
|
||||
assert.Equal(t, result, result2, "partMd5 function should be deterministic")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLbStatusListV1AndV1Beta1(t *testing.T) {
|
||||
clusterPrefix = "gw-123-"
|
||||
svcName := clusterPrefix
|
||||
svcList := []*v1.Service{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: svcName,
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Type: v1.ServiceTypeLoadBalancer,
|
||||
},
|
||||
Status: v1.ServiceStatus{
|
||||
LoadBalancer: v1.LoadBalancerStatus{
|
||||
Ingress: []v1.LoadBalancerIngress{
|
||||
{
|
||||
IP: "2.2.2.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: svcName,
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Type: v1.ServiceTypeLoadBalancer,
|
||||
},
|
||||
Status: v1.ServiceStatus{
|
||||
LoadBalancer: v1.LoadBalancerStatus{
|
||||
Ingress: []v1.LoadBalancerIngress{
|
||||
{
|
||||
Hostname: "1.1.1.1" + SvcHostNameSuffix,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test the V1 version
|
||||
t.Run("GetLbStatusListV1", func(t *testing.T) {
|
||||
lbiList := GetLbStatusListV1(svcList)
|
||||
|
||||
assert.Equal(t, 2, len(lbiList), "There should be 2 entry points")
|
||||
assert.Equal(t, "1.1.1.1", lbiList[0].IP, "The first IP should be 1.1.1.1")
|
||||
assert.Equal(t, "2.2.2.2", lbiList[1].IP, "The second IP should be 2.2.2.2")
|
||||
})
|
||||
|
||||
// Test the V1Beta1 version
|
||||
t.Run("GetLbStatusListV1Beta1", func(t *testing.T) {
|
||||
lbiList := GetLbStatusListV1Beta1(svcList)
|
||||
|
||||
assert.Equal(t, 2, len(lbiList), "There should be 2 entry points")
|
||||
assert.Equal(t, "1.1.1.1", lbiList[0].IP, "The first IP should be 1.1.1.1")
|
||||
assert.Equal(t, "2.2.2.2", lbiList[1].IP, "The second IP should be 2.2.2.2")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ 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"
|
||||
@@ -58,6 +59,7 @@ type ItemController interface {
|
||||
ValidHigressConfig(higressConfig *HigressConfig) error
|
||||
ConstructEnvoyFilters() ([]*config.Config, error)
|
||||
RegisterItemEventHandler(eventHandler ItemEventHandler)
|
||||
RegisterMcpReconciler(reconciler *reconcile.Reconciler)
|
||||
}
|
||||
|
||||
type ConfigmapMgr struct {
|
||||
@@ -111,6 +113,12 @@ func (c *ConfigmapMgr) GetHigressConfig() *HigressConfig {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ConfigmapMgr) SetMcpReconciler(reconciler *reconcile.Reconciler) {
|
||||
for _, itemController := range c.ItemControllers {
|
||||
itemController.RegisterMcpReconciler(reconciler)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ConfigmapMgr) AddItemControllers(controllers ...ItemController) {
|
||||
c.ItemControllers = append(c.ItemControllers, controllers...)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ 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"
|
||||
@@ -376,6 +377,9 @@ 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
|
||||
|
||||
@@ -23,6 +23,7 @@ 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"
|
||||
@@ -291,6 +292,9 @@ 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 := ""
|
||||
|
||||
@@ -24,6 +24,7 @@ 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"
|
||||
@@ -61,6 +62,8 @@ type SSEServer struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
// Additional Config parameters for the real MCP server implementation
|
||||
Config map[string]interface{} `json:"config,omitempty"`
|
||||
// The domain list of the SSE server
|
||||
DomainList []string `json:"domain_list,omitempty"`
|
||||
}
|
||||
|
||||
// MatchRule defines a rule for matching requests
|
||||
@@ -179,9 +182,10 @@ func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
|
||||
newMcp.Servers = make([]*SSEServer, len(mcp.Servers))
|
||||
for i, server := range mcp.Servers {
|
||||
newServer := &SSEServer{
|
||||
Name: server.Name,
|
||||
Path: server.Path,
|
||||
Type: server.Type,
|
||||
Name: server.Name,
|
||||
Path: server.Path,
|
||||
Type: server.Type,
|
||||
DomainList: server.DomainList,
|
||||
}
|
||||
if server.Config != nil {
|
||||
newServer.Config = make(map[string]interface{})
|
||||
@@ -212,6 +216,7 @@ type McpServerController struct {
|
||||
mcpServer atomic.Value
|
||||
Name string
|
||||
eventHandler ItemEventHandler
|
||||
reconclier *reconcile.Reconciler
|
||||
}
|
||||
|
||||
func NewMcpServerController(namespace string) *McpServerController {
|
||||
@@ -285,6 +290,10 @@ func (m *McpServerController) RegisterItemEventHandler(eventHandler ItemEventHan
|
||||
m.eventHandler = eventHandler
|
||||
}
|
||||
|
||||
func (m *McpServerController) RegisterMcpReconciler(reconciler *reconcile.Reconciler) {
|
||||
m.reconclier = reconciler
|
||||
}
|
||||
|
||||
func (m *McpServerController) ConstructEnvoyFilters() ([]*config.Config, error) {
|
||||
configs := make([]*config.Config, 0)
|
||||
mcpServer := m.GetMcpServer()
|
||||
@@ -294,88 +303,122 @@ func (m *McpServerController) ConstructEnvoyFilters() ([]*config.Config, error)
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
mcpStruct := m.constructMcpServerStruct(mcpServer)
|
||||
if mcpStruct == "" {
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
config := &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.EnvoyFilter,
|
||||
Name: higressMcpServerEnvoyFilterName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: &networking.EnvoyFilter{
|
||||
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
|
||||
{
|
||||
ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
|
||||
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
|
||||
Context: networking.EnvoyFilter_GATEWAY,
|
||||
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
|
||||
Listener: &networking.EnvoyFilter_ListenerMatch{
|
||||
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
|
||||
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
|
||||
Name: "envoy.filters.network.http_connection_manager",
|
||||
SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{
|
||||
Name: "envoy.filters.http.cors",
|
||||
// mcp-session envoy filter
|
||||
mcpSessionStruct := m.constructMcpSessionStruct(mcpServer)
|
||||
if mcpSessionStruct != "" {
|
||||
sessionConfig := &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.EnvoyFilter,
|
||||
Name: higressMcpServerEnvoyFilterName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: &networking.EnvoyFilter{
|
||||
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
|
||||
{
|
||||
ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
|
||||
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
|
||||
Context: networking.EnvoyFilter_GATEWAY,
|
||||
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
|
||||
Listener: &networking.EnvoyFilter_ListenerMatch{
|
||||
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
|
||||
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
|
||||
Name: "envoy.filters.network.http_connection_manager",
|
||||
SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{
|
||||
Name: "envoy.filters.http.cors",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Patch: &networking.EnvoyFilter_Patch{
|
||||
Operation: networking.EnvoyFilter_Patch_INSERT_AFTER,
|
||||
Value: util.BuildPatchStruct(mcpStruct),
|
||||
Patch: &networking.EnvoyFilter_Patch{
|
||||
Operation: networking.EnvoyFilter_Patch_INSERT_AFTER,
|
||||
Value: util.BuildPatchStruct(mcpSessionStruct),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
configs = append(configs, sessionConfig)
|
||||
}
|
||||
|
||||
// mcp-server envoy filter
|
||||
mcpServerStruct := m.constructMcpServerStruct(mcpServer)
|
||||
if mcpServerStruct != "" {
|
||||
serverConfig := &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.EnvoyFilter,
|
||||
Name: higressMcpServerEnvoyFilterName + "-server",
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: &networking.EnvoyFilter{
|
||||
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
|
||||
{
|
||||
ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
|
||||
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
|
||||
Context: networking.EnvoyFilter_GATEWAY,
|
||||
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
|
||||
Listener: &networking.EnvoyFilter_ListenerMatch{
|
||||
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
|
||||
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
|
||||
Name: "envoy.filters.network.http_connection_manager",
|
||||
SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{
|
||||
Name: "envoy.filters.http.router",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Patch: &networking.EnvoyFilter_Patch{
|
||||
Operation: networking.EnvoyFilter_Patch_INSERT_BEFORE,
|
||||
Value: util.BuildPatchStruct(mcpServerStruct),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
configs = append(configs, serverConfig)
|
||||
}
|
||||
|
||||
configs = append(configs, config)
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
|
||||
// Build servers configuration
|
||||
servers := "[]"
|
||||
if len(mcp.Servers) > 0 {
|
||||
serverConfigs := make([]string, len(mcp.Servers))
|
||||
for i, server := range mcp.Servers {
|
||||
serverConfig := fmt.Sprintf(`{
|
||||
"name": "%s",
|
||||
"path": "%s",
|
||||
"type": "%s"`,
|
||||
server.Name, server.Path, server.Type)
|
||||
|
||||
if len(server.Config) > 0 {
|
||||
config, _ := json.Marshal(server.Config)
|
||||
serverConfig += fmt.Sprintf(`,
|
||||
"config": %s`, string(config))
|
||||
}
|
||||
|
||||
serverConfig += "}"
|
||||
serverConfigs[i] = serverConfig
|
||||
}
|
||||
servers = fmt.Sprintf("[%s]", strings.Join(serverConfigs, ","))
|
||||
}
|
||||
|
||||
func (m *McpServerController) constructMcpSessionStruct(mcp *McpServer) string {
|
||||
// Build match_list configuration
|
||||
matchList := "[]"
|
||||
var matchConfigs []string
|
||||
if len(mcp.MatchList) > 0 {
|
||||
matchConfigs := make([]string, len(mcp.MatchList))
|
||||
for i, rule := range mcp.MatchList {
|
||||
matchConfigs[i] = fmt.Sprintf(`{
|
||||
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)
|
||||
}`, rule.MatchRuleDomain, rule.MatchRulePath, rule.MatchRuleType))
|
||||
}
|
||||
matchList = fmt.Sprintf("[%s]", strings.Join(matchConfigs, ","))
|
||||
}
|
||||
|
||||
// 构建 Redis 配置
|
||||
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]
|
||||
}
|
||||
path := vs.Http[0].Match[0].Uri.GetPrefix()
|
||||
matchConfigs = append(matchConfigs, fmt.Sprintf(`{
|
||||
"match_rule_domain": "%s",
|
||||
"match_rule_path": "%s",
|
||||
"match_rule_type": "prefix"
|
||||
}`, host, path))
|
||||
}
|
||||
}
|
||||
matchList = fmt.Sprintf("[%s]", strings.Join(matchConfigs, ","))
|
||||
|
||||
// Build redis configuration
|
||||
redisConfig := "null"
|
||||
if mcp.Redis != nil {
|
||||
redisConfig = fmt.Sprintf(`{
|
||||
@@ -386,7 +429,7 @@ func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
|
||||
}`, mcp.Redis.Address, mcp.Redis.Username, mcp.Redis.Password, mcp.Redis.DB)
|
||||
}
|
||||
|
||||
// 构建限流配置
|
||||
// Build rate limit configuration
|
||||
rateLimitConfig := "null"
|
||||
if mcp.Ratelimit != nil {
|
||||
whiteList := "[]"
|
||||
@@ -417,7 +460,6 @@ func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
|
||||
"rate_limit": %s,
|
||||
"sse_path_suffix": "%s",
|
||||
"match_list": %s,
|
||||
"servers": %s,
|
||||
"enable_user_level_server": %t
|
||||
}
|
||||
}
|
||||
@@ -428,6 +470,53 @@ func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
|
||||
rateLimitConfig,
|
||||
mcp.SsePathSuffix,
|
||||
matchList,
|
||||
servers,
|
||||
mcp.EnableUserLevelServer)
|
||||
}
|
||||
|
||||
func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
|
||||
// Build servers configuration
|
||||
servers := "[]"
|
||||
if len(mcp.Servers) > 0 {
|
||||
serverConfigs := make([]string, len(mcp.Servers))
|
||||
for i, server := range mcp.Servers {
|
||||
serverConfig := fmt.Sprintf(`{
|
||||
"name": "%s",
|
||||
"path": "%s",
|
||||
"type": "%s"`,
|
||||
server.Name, server.Path, server.Type)
|
||||
if len(server.DomainList) > 0 {
|
||||
domainList := fmt.Sprintf(`["%s"]`, strings.Join(server.DomainList, `","`))
|
||||
serverConfig += fmt.Sprintf(`,
|
||||
"domain_list": %s`, domainList)
|
||||
}
|
||||
if len(server.Config) > 0 {
|
||||
config, _ := json.Marshal(server.Config)
|
||||
serverConfig += fmt.Sprintf(`,
|
||||
"config": %s`, string(config))
|
||||
}
|
||||
serverConfig += "}"
|
||||
serverConfigs[i] = serverConfig
|
||||
}
|
||||
servers = fmt.Sprintf("[%s]", strings.Join(serverConfigs, ","))
|
||||
}
|
||||
|
||||
// Build complete configuration structure
|
||||
return fmt.Sprintf(`{
|
||||
"name": "envoy.filters.http.golang",
|
||||
"typed_config": {
|
||||
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
|
||||
"type_url": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
|
||||
"value": {
|
||||
"library_id": "mcp-server",
|
||||
"library_path": "/var/lib/istio/envoy/golang-filter.so",
|
||||
"plugin_name": "mcp-server",
|
||||
"plugin_config": {
|
||||
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
|
||||
"value": {
|
||||
"servers": %s
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`, servers)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package configmap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
@@ -422,3 +423,311 @@ func TestMcpServerController_AddOrUpdateHigressConfig(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMcpServerController_ValidHigressConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
higressConfig *HigressConfig
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "nil config",
|
||||
higressConfig: nil,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "nil mcp server",
|
||||
higressConfig: &HigressConfig{
|
||||
McpServer: nil,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "valid config",
|
||||
higressConfig: &HigressConfig{
|
||||
McpServer: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
},
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid config - user level server without redis",
|
||||
higressConfig: &HigressConfig{
|
||||
McpServer: &McpServer{
|
||||
Enable: true,
|
||||
EnableUserLevelServer: true,
|
||||
Redis: nil,
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
},
|
||||
wantErr: errors.New("redis config cannot be empty when user level server is enabled"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := NewMcpServerController("test-namespace")
|
||||
err := m.ValidHigressConfig(tt.higressConfig)
|
||||
assert.Equal(t, tt.wantErr, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMcpServerController_ConstructEnvoyFilters(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mcpServer *McpServer
|
||||
wantConfigs int
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "nil mcp server",
|
||||
mcpServer: nil,
|
||||
wantConfigs: 0,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "disabled mcp server",
|
||||
mcpServer: &McpServer{
|
||||
Enable: false,
|
||||
},
|
||||
wantConfigs: 0,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "valid mcp server with redis",
|
||||
mcpServer: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
},
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantConfigs: 2, // Both session and server filters
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := NewMcpServerController("test-namespace")
|
||||
m.mcpServer.Store(tt.mcpServer)
|
||||
configs, err := m.ConstructEnvoyFilters()
|
||||
assert.Equal(t, tt.wantErr, err)
|
||||
assert.Equal(t, tt.wantConfigs, len(configs))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMcpServerController_constructMcpSessionStruct(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mcp *McpServer
|
||||
wantJSON string
|
||||
}{
|
||||
{
|
||||
name: "minimal config",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
},
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantJSON: `{
|
||||
"name": "envoy.filters.http.golang",
|
||||
"typed_config": {
|
||||
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
|
||||
"type_url": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
|
||||
"value": {
|
||||
"library_id": "mcp-session",
|
||||
"library_path": "/var/lib/istio/envoy/golang-filter.so",
|
||||
"plugin_name": "mcp-session",
|
||||
"plugin_config": {
|
||||
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
|
||||
"value": {
|
||||
"redis": {
|
||||
"address": "localhost:6379",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"db": 0
|
||||
},
|
||||
"rate_limit": null,
|
||||
"sse_path_suffix": "",
|
||||
"match_list": [],
|
||||
"enable_user_level_server": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "full config",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
DB: 1,
|
||||
},
|
||||
SsePathSuffix: "/sse",
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "/test",
|
||||
MatchRuleType: "exact",
|
||||
},
|
||||
},
|
||||
EnableUserLevelServer: true,
|
||||
Ratelimit: &MCPRatelimitConfig{
|
||||
Limit: 100,
|
||||
Window: 3600,
|
||||
WhiteList: []string{"user1", "user2"},
|
||||
},
|
||||
},
|
||||
wantJSON: `{
|
||||
"name": "envoy.filters.http.golang",
|
||||
"typed_config": {
|
||||
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
|
||||
"type_url": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
|
||||
"value": {
|
||||
"library_id": "mcp-session",
|
||||
"library_path": "/var/lib/istio/envoy/golang-filter.so",
|
||||
"plugin_name": "mcp-session",
|
||||
"plugin_config": {
|
||||
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
|
||||
"value": {
|
||||
"redis": {
|
||||
"address": "localhost:6379",
|
||||
"username": "user",
|
||||
"password": "pass",
|
||||
"db": 1
|
||||
},
|
||||
"rate_limit": {
|
||||
"limit": 100,
|
||||
"window": 3600,
|
||||
"white_list": ["user1","user2"]
|
||||
},
|
||||
"sse_path_suffix": "/sse",
|
||||
"match_list": [{
|
||||
"match_rule_domain": "*",
|
||||
"match_rule_path": "/test",
|
||||
"match_rule_type": "exact"
|
||||
}],
|
||||
"enable_user_level_server": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := NewMcpServerController("test-namespace")
|
||||
got := m.constructMcpSessionStruct(tt.mcp)
|
||||
// Normalize JSON strings for comparison
|
||||
var gotJSON, wantJSON interface{}
|
||||
json.Unmarshal([]byte(got), &gotJSON)
|
||||
json.Unmarshal([]byte(tt.wantJSON), &wantJSON)
|
||||
assert.Equal(t, wantJSON, gotJSON)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMcpServerController_constructMcpServerStruct(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mcp *McpServer
|
||||
wantJSON string
|
||||
}{
|
||||
{
|
||||
name: "no servers",
|
||||
mcp: &McpServer{
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantJSON: `{
|
||||
"name": "envoy.filters.http.golang",
|
||||
"typed_config": {
|
||||
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
|
||||
"type_url": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
|
||||
"value": {
|
||||
"library_id": "mcp-server",
|
||||
"library_path": "/var/lib/istio/envoy/golang-filter.so",
|
||||
"plugin_name": "mcp-server",
|
||||
"plugin_config": {
|
||||
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
|
||||
"value": {
|
||||
"servers": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "with servers",
|
||||
mcp: &McpServer{
|
||||
Servers: []*SSEServer{
|
||||
{
|
||||
Name: "test-server",
|
||||
Path: "/test",
|
||||
Type: "test",
|
||||
Config: map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
DomainList: []string{"example.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantJSON: `{
|
||||
"name": "envoy.filters.http.golang",
|
||||
"typed_config": {
|
||||
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
|
||||
"type_url": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
|
||||
"value": {
|
||||
"library_id": "mcp-server",
|
||||
"library_path": "/var/lib/istio/envoy/golang-filter.so",
|
||||
"plugin_name": "mcp-server",
|
||||
"plugin_config": {
|
||||
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
|
||||
"value": {
|
||||
"servers": [{
|
||||
"name": "test-server",
|
||||
"path": "/test",
|
||||
"type": "test",
|
||||
"domain_list": ["example.com"],
|
||||
"config": {"key":"value"}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := NewMcpServerController("test-namespace")
|
||||
got := m.constructMcpServerStruct(tt.mcp)
|
||||
// Normalize JSON strings for comparison
|
||||
var gotJSON, wantJSON interface{}
|
||||
json.Unmarshal([]byte(got), &gotJSON)
|
||||
json.Unmarshal([]byte(tt.wantJSON), &wantJSON)
|
||||
assert.Equal(t, wantJSON, gotJSON)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/alibaba/higress/registry/reconcile"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
|
||||
@@ -237,6 +238,9 @@ 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()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.23-bullseye AS golang-base
|
||||
FROM golang:1.22-bullseye AS golang-base
|
||||
|
||||
ARG GOPROXY
|
||||
ARG GO_FILTER_NAME
|
||||
@@ -24,7 +24,7 @@ WORKDIR /workspace
|
||||
|
||||
COPY . .
|
||||
|
||||
WORKDIR /workspace/$GO_FILTER_NAME
|
||||
WORKDIR /workspace
|
||||
|
||||
RUN go mod tidy
|
||||
RUN if [ "$GOARCH" = "arm64" ]; then \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
GO_FILTER_NAME ?= mcp-server
|
||||
GO_FILTER_NAME ?= golang-filter
|
||||
GOPROXY := $(shell go env GOPROXY)
|
||||
GOARCH ?= amd64
|
||||
|
||||
@@ -8,5 +8,5 @@ build:
|
||||
--build-arg GO_FILTER_NAME=${GO_FILTER_NAME} \
|
||||
--build-arg GOARCH=${GOARCH} \
|
||||
-t ${GO_FILTER_NAME} \
|
||||
--output ./${GO_FILTER_NAME} \
|
||||
--output . \
|
||||
.
|
||||
@@ -28,7 +28,7 @@ http_filters:
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config
|
||||
library_id: my-go-filter
|
||||
library_path: "./my-go-filter.so"
|
||||
library_path: "./go-filter.so"
|
||||
plugin_name: my-go-filter
|
||||
plugin_config:
|
||||
"@type": type.googleapis.com/xds.type.v3.TypedStruct
|
||||
@@ -43,5 +43,5 @@ http_filters:
|
||||
使用以下命令可以快速构建 golang filter 插件:
|
||||
|
||||
```bash
|
||||
GO_FILTER_NAME=mcp-server make build
|
||||
make build
|
||||
```
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
module github.com/alibaba/higress/plugins/golang-filter/mcp-server
|
||||
module github.com/alibaba/higress/plugins/golang-filter
|
||||
|
||||
go 1.23
|
||||
go 1.22
|
||||
|
||||
replace github.com/envoyproxy/envoy => github.com/higress-group/envoy v0.0.0-20250428030521-17cf01d9f644
|
||||
|
||||
replace github.com/mark3labs/mcp-go => github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30
|
||||
|
||||
require (
|
||||
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42
|
||||
@@ -136,8 +136,6 @@ github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9r
|
||||
github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/envoyproxy/envoy v1.33.1-0.20250325161043-11ab50a29d99 h1:jih/Ieb7BFgVCStgvY5fXQ3mI9ByOt4wfwUF0d7qmqI=
|
||||
github.com/envoyproxy/envoy v1.33.1-0.20250325161043-11ab50a29d99/go.mod h1:x7d0dNbE0xGuDBUkBg19VGCgnPQ+lJ2k8lDzDzKExow=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@@ -236,6 +234,10 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO
|
||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/higress-group/envoy v0.0.0-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/mcp-go v0.0.0-20250428145706-792ce64b4b30 h1:N4NMq8M1nZyyChPyzn+EUUdHi5asig2uLR5hOyRmsXI=
|
||||
github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30/go.mod h1:O9gri9UOzthw728vusc2oNu99lVh8cKCajpxNfC90gE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
@@ -283,8 +285,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-40 h1:nzRTBplC0riQqQwEHZThw5H4/TH5LgWTQTm6A7t1lpY=
|
||||
github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-40/go.mod h1:9FKXl6FqOiVmm72i8kADtbeK71egyG9y3uRDBg41tpQ=
|
||||
github.com/mark3labs/mcp-go v0.12.0 h1:Pue1Tdwqcz77GHq18uzgmLT3wmeDUxXUSAqSwhGLhVo=
|
||||
github.com/mark3labs/mcp-go v0.12.0/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
25
plugins/golang-filter/main.go
Normal file
25
plugins/golang-filter/main.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
mcp_server "github.com/alibaba/higress/plugins/golang-filter/mcp-server"
|
||||
mcp_session "github.com/alibaba/higress/plugins/golang-filter/mcp-session"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
envoyHttp "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http"
|
||||
)
|
||||
|
||||
func init() {
|
||||
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(mcp_session.Name, mcp_session.FilterFactory, &mcp_session.Parser{})
|
||||
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(mcp_server.Name, mcp_server.FilterFactory, &mcp_server.Parser{})
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
api.LogErrorf("PProf server recovered from panic: %v", r)
|
||||
}
|
||||
}()
|
||||
api.LogError(http.ListenAndServe("localhost:6060", nil).Error())
|
||||
}()
|
||||
}
|
||||
|
||||
func main() {}
|
||||
@@ -1,64 +1,39 @@
|
||||
package main
|
||||
package mcp_server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
|
||||
xds "github.com/cncf/xds/go/xds/type/v3"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/handler"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/registry/nacos"
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/gorm"
|
||||
mcp_session "github.com/alibaba/higress/plugins/golang-filter/mcp-session"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
xds "github.com/cncf/xds/go/xds/type/v3"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
envoyHttp "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
)
|
||||
|
||||
const Name = "mcp-session"
|
||||
const Name = "mcp-server"
|
||||
const Version = "1.0.0"
|
||||
const DefaultServerName = "defaultServer"
|
||||
const ConfigPathSuffix = "/config"
|
||||
|
||||
func init() {
|
||||
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(Name, filterFactory, &parser{})
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
api.LogErrorf("PProf server recovered from panic: %v", r)
|
||||
}
|
||||
}()
|
||||
api.LogError(http.ListenAndServe("localhost:6060", nil).Error())
|
||||
}()
|
||||
type SSEServerWrapper struct {
|
||||
BaseServer *common.SSEServer
|
||||
DomainList []string
|
||||
}
|
||||
|
||||
type config struct {
|
||||
ssePathSuffix string
|
||||
redisClient *internal.RedisClient
|
||||
servers []*internal.SSEServer
|
||||
defaultServer *internal.SSEServer
|
||||
matchList []internal.MatchRule
|
||||
enableUserLevelServer bool
|
||||
rateLimitConfig *handler.MCPRatelimitConfig
|
||||
servers []*SSEServerWrapper
|
||||
}
|
||||
|
||||
func (c *config) Destroy() {
|
||||
if c.redisClient != nil {
|
||||
api.LogDebug("Closing Redis client")
|
||||
c.redisClient.Close()
|
||||
}
|
||||
for _, server := range c.servers {
|
||||
server.Close()
|
||||
server.BaseServer.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type parser struct {
|
||||
type Parser struct {
|
||||
}
|
||||
|
||||
// Parse the filter configuration
|
||||
func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (interface{}, error) {
|
||||
func (p *Parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (interface{}, error) {
|
||||
configStruct := &xds.TypedStruct{}
|
||||
if err := any.UnmarshalTo(configStruct); err != nil {
|
||||
return nil, err
|
||||
@@ -66,82 +41,9 @@ func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
|
||||
v := configStruct.Value
|
||||
|
||||
conf := &config{
|
||||
matchList: make([]internal.MatchRule, 0),
|
||||
servers: make([]*internal.SSEServer, 0),
|
||||
servers: make([]*SSEServerWrapper, 0),
|
||||
}
|
||||
|
||||
// Parse match_list if exists
|
||||
if matchList, ok := v.AsMap()["match_list"].([]interface{}); ok {
|
||||
for _, item := range matchList {
|
||||
if ruleMap, ok := item.(map[string]interface{}); ok {
|
||||
rule := internal.MatchRule{}
|
||||
if domain, ok := ruleMap["match_rule_domain"].(string); ok {
|
||||
rule.MatchRuleDomain = domain
|
||||
}
|
||||
if path, ok := ruleMap["match_rule_path"].(string); ok {
|
||||
rule.MatchRulePath = path
|
||||
}
|
||||
if ruleType, ok := ruleMap["match_rule_type"].(string); ok {
|
||||
rule.MatchRuleType = internal.RuleType(ruleType)
|
||||
}
|
||||
conf.matchList = append(conf.matchList, rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Redis configuration is optional
|
||||
if redisConfigMap, ok := v.AsMap()["redis"].(map[string]interface{}); ok {
|
||||
redisConfig, err := internal.ParseRedisConfig(redisConfigMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse redis config: %w", err)
|
||||
}
|
||||
|
||||
redisClient, err := internal.NewRedisClient(redisConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize RedisClient: %w", err)
|
||||
}
|
||||
conf.redisClient = redisClient
|
||||
api.LogDebug("Redis client initialized")
|
||||
} else {
|
||||
api.LogDebug("Redis configuration not provided, running without Redis")
|
||||
}
|
||||
|
||||
enableUserLevelServer, ok := v.AsMap()["enable_user_level_server"].(bool)
|
||||
if !ok {
|
||||
enableUserLevelServer = false
|
||||
if conf.redisClient == nil {
|
||||
return nil, fmt.Errorf("redis configuration is not provided, enable_user_level_server is true")
|
||||
}
|
||||
}
|
||||
conf.enableUserLevelServer = enableUserLevelServer
|
||||
|
||||
if rateLimit, ok := v.AsMap()["rate_limit"].(map[string]interface{}); ok {
|
||||
rateLimitConfig := &handler.MCPRatelimitConfig{}
|
||||
if limit, ok := rateLimit["limit"].(float64); ok {
|
||||
rateLimitConfig.Limit = int(limit)
|
||||
}
|
||||
if window, ok := rateLimit["window"].(float64); ok {
|
||||
rateLimitConfig.Window = int(window)
|
||||
}
|
||||
if whiteList, ok := rateLimit["white_list"].([]interface{}); ok {
|
||||
for _, item := range whiteList {
|
||||
if uid, ok := item.(string); ok {
|
||||
rateLimitConfig.Whitelist = append(rateLimitConfig.Whitelist, uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
if errorText, ok := rateLimit["error_text"].(string); ok {
|
||||
rateLimitConfig.ErrorText = errorText
|
||||
}
|
||||
conf.rateLimitConfig = rateLimitConfig
|
||||
}
|
||||
|
||||
ssePathSuffix, ok := v.AsMap()["sse_path_suffix"].(string)
|
||||
if !ok || ssePathSuffix == "" {
|
||||
return nil, fmt.Errorf("sse path suffix is not set or empty")
|
||||
}
|
||||
conf.ssePathSuffix = ssePathSuffix
|
||||
|
||||
serverConfigs, ok := v.AsMap()["servers"].([]interface{})
|
||||
if !ok {
|
||||
api.LogDebug("No servers are configured")
|
||||
@@ -153,19 +55,33 @@ func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("server config must be an object")
|
||||
}
|
||||
|
||||
serverType, ok := serverConfigMap["type"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("server type is not set")
|
||||
}
|
||||
|
||||
serverPath, ok := serverConfigMap["path"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("server %s path is not set", serverType)
|
||||
}
|
||||
|
||||
serverDomainList := []string{}
|
||||
if domainList, ok := serverConfigMap["domain_list"].([]interface{}); ok {
|
||||
for _, domain := range domainList {
|
||||
if domainStr, ok := domain.(string); ok {
|
||||
serverDomainList = append(serverDomainList, domainStr)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
serverDomainList = []string{"*"}
|
||||
}
|
||||
|
||||
serverName, ok := serverConfigMap["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("server %s name is not set", serverType)
|
||||
}
|
||||
server := internal.GlobalRegistry.GetServer(serverType)
|
||||
server := common.GlobalRegistry.GetServer(serverType)
|
||||
|
||||
if server == nil {
|
||||
return nil, fmt.Errorf("server %s is not registered", serverType)
|
||||
@@ -186,50 +102,36 @@ func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
|
||||
return nil, fmt.Errorf("failed to initialize DBServer: %w", err)
|
||||
}
|
||||
|
||||
conf.servers = append(conf.servers, internal.NewSSEServer(serverInstance,
|
||||
internal.WithRedisClient(conf.redisClient),
|
||||
internal.WithSSEEndpoint(fmt.Sprintf("%s%s", serverPath, ssePathSuffix)),
|
||||
internal.WithMessageEndpoint(serverPath)))
|
||||
conf.servers = append(conf.servers, &SSEServerWrapper{
|
||||
BaseServer: common.NewSSEServer(serverInstance,
|
||||
common.WithSSEEndpoint(fmt.Sprintf("%s%s", serverPath, mcp_session.GlobalSSEPathSuffix)),
|
||||
common.WithMessageEndpoint(serverPath)),
|
||||
DomainList: serverDomainList,
|
||||
})
|
||||
api.LogDebug(fmt.Sprintf("Registered MCP Server: %s", serverType))
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func (p *parser) Merge(parent interface{}, child interface{}) interface{} {
|
||||
func (p *Parser) Merge(parent interface{}, child interface{}) interface{} {
|
||||
parentConfig := parent.(*config)
|
||||
childConfig := child.(*config)
|
||||
|
||||
newConfig := *parentConfig
|
||||
if childConfig.redisClient != nil {
|
||||
newConfig.redisClient = childConfig.redisClient
|
||||
}
|
||||
if childConfig.ssePathSuffix != "" {
|
||||
newConfig.ssePathSuffix = childConfig.ssePathSuffix
|
||||
}
|
||||
if childConfig.servers != nil {
|
||||
newConfig.servers = childConfig.servers
|
||||
}
|
||||
if childConfig.defaultServer != nil {
|
||||
newConfig.defaultServer = childConfig.defaultServer
|
||||
}
|
||||
if childConfig.matchList != nil {
|
||||
newConfig.matchList = childConfig.matchList
|
||||
}
|
||||
return &newConfig
|
||||
}
|
||||
|
||||
func filterFactory(c interface{}, callbacks api.FilterCallbackHandler) api.StreamFilter {
|
||||
func FilterFactory(c interface{}, callbacks api.FilterCallbackHandler) api.StreamFilter {
|
||||
conf, ok := c.(*config)
|
||||
if !ok {
|
||||
panic("unexpected config type")
|
||||
}
|
||||
return &filter{
|
||||
callbacks: callbacks,
|
||||
config: conf,
|
||||
stopChan: make(chan struct{}),
|
||||
mcpConfigHandler: handler.NewMCPConfigHandler(conf.redisClient, callbacks),
|
||||
mcpRatelimitHandler: handler.NewMCPRatelimitHandler(conf.redisClient, callbacks, conf.rateLimitConfig),
|
||||
config: conf,
|
||||
callbacks: callbacks,
|
||||
}
|
||||
}
|
||||
|
||||
func main() {}
|
||||
|
||||
@@ -1,104 +1,44 @@
|
||||
package main
|
||||
package mcp_server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/handler"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
const (
|
||||
RedisNotEnabledResponseBody = "Redis is not enabled, SSE connection is not supported"
|
||||
)
|
||||
|
||||
// The callbacks in the filter, like `DecodeHeaders`, can be implemented on demand.
|
||||
// Because api.PassThroughStreamFilter provides a default implementation.
|
||||
type filter struct {
|
||||
api.PassThroughStreamFilter
|
||||
|
||||
callbacks api.FilterCallbackHandler
|
||||
path string
|
||||
config *config
|
||||
stopChan chan struct{}
|
||||
|
||||
req *http.Request
|
||||
serverName string
|
||||
message bool
|
||||
proxyURL *url.URL
|
||||
skip bool
|
||||
|
||||
userLevelConfig bool
|
||||
mcpConfigHandler *handler.MCPConfigHandler
|
||||
ratelimit bool
|
||||
mcpRatelimitHandler *handler.MCPRatelimitHandler
|
||||
config *config
|
||||
req *http.Request
|
||||
message bool
|
||||
path string
|
||||
}
|
||||
|
||||
type RequestURL struct {
|
||||
method string
|
||||
scheme string
|
||||
host string
|
||||
path string
|
||||
baseURL string
|
||||
parsedURL *url.URL
|
||||
internalIP bool
|
||||
}
|
||||
|
||||
func NewRequestURL(header api.RequestHeaderMap) *RequestURL {
|
||||
method, _ := header.Get(":method")
|
||||
scheme, _ := header.Get(":scheme")
|
||||
host, _ := header.Get(":authority")
|
||||
path, _ := header.Get(":path")
|
||||
internalIP, _ := header.Get("x-envoy-internal")
|
||||
baseURL := fmt.Sprintf("%s://%s", scheme, host)
|
||||
parsedURL, _ := url.Parse(path)
|
||||
api.LogDebugf("RequestURL: method=%s, scheme=%s, host=%s, path=%s", method, scheme, host, path)
|
||||
return &RequestURL{method: method, scheme: scheme, host: host, path: path, baseURL: baseURL, parsedURL: parsedURL, internalIP: internalIP == "true"}
|
||||
}
|
||||
|
||||
// Callbacks which are called in request path
|
||||
// The endStream is true if the request doesn't have body
|
||||
func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.StatusType {
|
||||
url := NewRequestURL(header)
|
||||
f.path = url.parsedURL.Path
|
||||
|
||||
// Check if request matches any rule in match_list
|
||||
if !internal.IsMatch(f.config.matchList, url.host, f.path) {
|
||||
f.skip = true
|
||||
api.LogDebugf("Request does not match any rule in match_list: %s", url.parsedURL.String())
|
||||
url := common.NewRequestURL(header)
|
||||
if url == nil {
|
||||
return api.Continue
|
||||
}
|
||||
f.path = url.ParsedURL.Path
|
||||
|
||||
for _, server := range f.config.servers {
|
||||
if f.path == server.GetSSEEndpoint() {
|
||||
if url.method != http.MethodGet {
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
|
||||
} else {
|
||||
f.serverName = server.GetServerName()
|
||||
body := "SSE connection create"
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusOK, body, nil, 0, "")
|
||||
}
|
||||
api.LogDebugf("%s SSE connection started", server.GetServerName())
|
||||
return api.LocalReply
|
||||
} else if f.path == server.GetMessageEndpoint() {
|
||||
if url.method != http.MethodPost {
|
||||
if common.MatchDomainList(url.ParsedURL.Host, server.DomainList) && url.ParsedURL.Path == server.BaseServer.GetMessageEndpoint() {
|
||||
if url.Method != http.MethodPost {
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
// Create a new http.Request object
|
||||
f.req = &http.Request{
|
||||
Method: url.method,
|
||||
URL: url.parsedURL,
|
||||
Method: url.Method,
|
||||
URL: url.ParsedURL,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
api.LogDebugf("Message request: %v", url.parsedURL)
|
||||
api.LogDebugf("Message request: %v", url.ParsedURL)
|
||||
// Copy headers from api.RequestHeaderMap to http.Header
|
||||
header.Range(func(key, value string) bool {
|
||||
f.req.Header.Add(key, value)
|
||||
@@ -113,209 +53,33 @@ func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.
|
||||
}
|
||||
}
|
||||
|
||||
f.req = &http.Request{
|
||||
Method: url.method,
|
||||
URL: url.parsedURL,
|
||||
}
|
||||
|
||||
if strings.HasSuffix(f.path, ConfigPathSuffix) && f.config.enableUserLevelServer {
|
||||
if !url.internalIP {
|
||||
api.LogWarnf("Access denied: non-internal IP address %s", url.parsedURL.String())
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
if strings.HasSuffix(f.path, ConfigPathSuffix) && url.method == http.MethodGet {
|
||||
api.LogDebugf("Handling config request: %s", f.path)
|
||||
f.mcpConfigHandler.HandleConfigRequest(f.req, []byte{})
|
||||
return api.LocalReply
|
||||
}
|
||||
f.userLevelConfig = true
|
||||
if endStream {
|
||||
return api.Continue
|
||||
} else {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(url.parsedURL.Path, f.config.ssePathSuffix) {
|
||||
f.proxyURL = url.parsedURL
|
||||
if f.config.enableUserLevelServer {
|
||||
parts := strings.Split(url.parsedURL.Path, "/")
|
||||
if len(parts) >= 3 {
|
||||
serverName := parts[1]
|
||||
uid := parts[2]
|
||||
// Get encoded config
|
||||
encodedConfig, _ := f.mcpConfigHandler.GetEncodedConfig(serverName, uid)
|
||||
if encodedConfig != "" {
|
||||
header.Set("x-higress-mcpserver-config", encodedConfig)
|
||||
api.LogDebugf("Set x-higress-mcpserver-config Header for %s:%s", serverName, uid)
|
||||
}
|
||||
}
|
||||
f.ratelimit = true
|
||||
}
|
||||
if endStream {
|
||||
return api.Continue
|
||||
} else {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
}
|
||||
|
||||
if url.method != http.MethodGet {
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
|
||||
} else {
|
||||
f.config.defaultServer = internal.NewSSEServer(internal.NewMCPServer(DefaultServerName, Version),
|
||||
internal.WithSSEEndpoint(f.config.ssePathSuffix),
|
||||
internal.WithMessageEndpoint(strings.TrimSuffix(url.parsedURL.Path, f.config.ssePathSuffix)),
|
||||
internal.WithRedisClient(f.config.redisClient))
|
||||
f.serverName = f.config.defaultServer.GetServerName()
|
||||
body := "SSE connection create"
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusOK, body, nil, 0, "")
|
||||
}
|
||||
return api.LocalReply
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
// DecodeData might be called multiple times during handling the request body.
|
||||
// The endStream is true when handling the last piece of the body.
|
||||
func (f *filter) DecodeData(buffer api.BufferInstance, endStream bool) api.StatusType {
|
||||
if f.skip {
|
||||
return api.Continue
|
||||
}
|
||||
if !endStream {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
if f.message {
|
||||
for _, server := range f.config.servers {
|
||||
if f.path == server.GetMessageEndpoint() {
|
||||
if f.path == server.BaseServer.GetMessageEndpoint() {
|
||||
// Create a response recorder to capture the response
|
||||
recorder := httptest.NewRecorder()
|
||||
// Call the handleMessage method of SSEServer with complete body
|
||||
httpStatus := server.HandleMessage(recorder, f.req, buffer.Bytes())
|
||||
httpStatus := server.BaseServer.HandleMessage(recorder, f.req, buffer.Bytes())
|
||||
f.message = false
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(httpStatus, recorder.Body.String(), recorder.Header(), 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
}
|
||||
} else if f.userLevelConfig {
|
||||
// Handle config POST request
|
||||
api.LogDebugf("Handling config request: %s", f.path)
|
||||
f.mcpConfigHandler.HandleConfigRequest(f.req, buffer.Bytes())
|
||||
return api.LocalReply
|
||||
} else if f.ratelimit {
|
||||
if checkJSONRPCMethod(buffer.Bytes(), "tools/list") {
|
||||
api.LogDebugf("Not a tools call request, skipping ratelimit")
|
||||
return api.Continue
|
||||
}
|
||||
parts := strings.Split(f.req.URL.Path, "/")
|
||||
if len(parts) < 3 {
|
||||
api.LogWarnf("Access denied: no valid uid found")
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
serverName := parts[1]
|
||||
uid := parts[2]
|
||||
encodedConfig, err := f.mcpConfigHandler.GetEncodedConfig(serverName, uid)
|
||||
if err != nil {
|
||||
api.LogWarnf("Access denied: no valid config found for uid %s", uid)
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return api.LocalReply
|
||||
} else if encodedConfig == "" && checkJSONRPCMethod(buffer.Bytes(), "tools/call") {
|
||||
api.LogDebugf("Empty config found for %s:%s", serverName, uid)
|
||||
if !f.mcpRatelimitHandler.HandleRatelimit(f.req, buffer.Bytes()) {
|
||||
return api.LocalReply
|
||||
}
|
||||
}
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
// Callbacks which are called in response path
|
||||
// The endStream is true if the response doesn't have body
|
||||
func (f *filter) EncodeHeaders(header api.ResponseHeaderMap, endStream bool) api.StatusType {
|
||||
if f.skip {
|
||||
return api.Continue
|
||||
}
|
||||
if f.serverName != "" {
|
||||
if f.config.redisClient != nil {
|
||||
header.Set("Content-Type", "text/event-stream")
|
||||
header.Set("Cache-Control", "no-cache")
|
||||
header.Set("Connection", "keep-alive")
|
||||
header.Set("Access-Control-Allow-Origin", "*")
|
||||
header.Del("Content-Length")
|
||||
} else {
|
||||
header.Set("Content-Length", strconv.Itoa(len(RedisNotEnabledResponseBody)))
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
// EncodeData might be called multiple times during handling the response body.
|
||||
// The endStream is true when handling the last piece of the body.
|
||||
func (f *filter) EncodeData(buffer api.BufferInstance, endStream bool) api.StatusType {
|
||||
if f.skip {
|
||||
return api.Continue
|
||||
}
|
||||
if !endStream {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
if f.proxyURL != nil && f.config.redisClient != nil {
|
||||
sessionID := f.proxyURL.Query().Get("sessionId")
|
||||
if sessionID != "" {
|
||||
channel := internal.GetSSEChannelName(sessionID)
|
||||
eventData := fmt.Sprintf("event: message\ndata: %s\n\n", buffer.String())
|
||||
publishErr := f.config.redisClient.Publish(channel, eventData)
|
||||
if publishErr != nil {
|
||||
api.LogErrorf("Failed to publish wasm mcp server message to Redis: %v", publishErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if f.serverName != "" {
|
||||
if f.config.redisClient != nil {
|
||||
// handle specific server
|
||||
for _, server := range f.config.servers {
|
||||
if f.serverName == server.GetServerName() {
|
||||
buffer.Reset()
|
||||
server.HandleSSE(f.callbacks, f.stopChan)
|
||||
return api.Running
|
||||
}
|
||||
}
|
||||
// handle default server
|
||||
if f.serverName == f.config.defaultServer.GetServerName() {
|
||||
buffer.Reset()
|
||||
f.config.defaultServer.HandleSSE(f.callbacks, f.stopChan)
|
||||
return api.Running
|
||||
}
|
||||
return api.Continue
|
||||
} else {
|
||||
buffer.SetString(RedisNotEnabledResponseBody)
|
||||
return api.Continue
|
||||
}
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
// OnDestroy stops the goroutine
|
||||
func (f *filter) OnDestroy(reason api.DestroyReason) {
|
||||
api.LogDebugf("OnDestroy: reason=%v", reason)
|
||||
if f.serverName != "" && f.stopChan != nil {
|
||||
select {
|
||||
case <-f.stopChan:
|
||||
return
|
||||
default:
|
||||
api.LogDebug("Stopping SSE connection")
|
||||
close(f.stopChan)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if the request is a tools/call request
|
||||
func checkJSONRPCMethod(body []byte, method string) bool {
|
||||
var request mcp.CallToolRequest
|
||||
if err := json.Unmarshal(body, &request); err != nil {
|
||||
api.LogWarnf("Failed to unmarshal request body: %v, not a JSON RPC request", err)
|
||||
return true
|
||||
}
|
||||
|
||||
return request.Method == method
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/registry"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/nacos-group/nacos-sdk-go/v2/clients"
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
internal.GlobalRegistry.RegisterServer("nacos-mcp-registry", &NacosConfig{})
|
||||
common.GlobalRegistry.RegisterServer("nacos-mcp-registry", &NacosConfig{})
|
||||
}
|
||||
|
||||
type NacosConfig struct {
|
||||
@@ -28,7 +28,7 @@ type NacosConfig struct {
|
||||
}
|
||||
|
||||
type McpServerToolsChangeListener struct {
|
||||
mcpServer *internal.MCPServer
|
||||
mcpServer *common.MCPServer
|
||||
}
|
||||
|
||||
func (l *McpServerToolsChangeListener) OnToolChanged(reg registry.McpServerRegistry) {
|
||||
@@ -137,8 +137,8 @@ func (c *NacosConfig) ParseConfig(config map[string]any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *NacosConfig) NewServer(serverName string) (*internal.MCPServer, error) {
|
||||
mcpServer := internal.NewMCPServer(
|
||||
func (c *NacosConfig) NewServer(serverName string) (*common.MCPServer, error) {
|
||||
mcpServer := common.NewMCPServer(
|
||||
serverName,
|
||||
"1.0.0",
|
||||
)
|
||||
@@ -170,11 +170,11 @@ func (c *NacosConfig) NewServer(serverName string) (*internal.MCPServer, error)
|
||||
return mcpServer, nil
|
||||
}
|
||||
|
||||
func resetToolsToMcpServer(mcpServer *internal.MCPServer, reg registry.McpServerRegistry) {
|
||||
wrappedTools := []internal.ServerTool{}
|
||||
func resetToolsToMcpServer(mcpServer *common.MCPServer, reg registry.McpServerRegistry) {
|
||||
wrappedTools := []common.ServerTool{}
|
||||
tools := reg.ListToolsDesciption()
|
||||
for _, tool := range tools {
|
||||
wrappedTools = append(wrappedTools, internal.ServerTool{
|
||||
wrappedTools = append(wrappedTools, common.ServerTool{
|
||||
Tool: mcp.NewToolWithRawSchema(tool.Name, tool.Description, tool.InputSchema),
|
||||
Handler: registry.HandleRegistryToolsCall(reg),
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
@@ -204,7 +204,7 @@ func CommonRemoteCall(reg McpServerRegistry, toolName string, parameters map[str
|
||||
return remoteHandle.HandleToolCall(ctx, parameters)
|
||||
}
|
||||
|
||||
func HandleRegistryToolsCall(reg McpServerRegistry) internal.ToolHandlerFunc {
|
||||
func HandleRegistryToolsCall(reg McpServerRegistry) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
return CommonRemoteCall(reg, request.Params.Name, arguments)
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
const Version = "1.0.0"
|
||||
|
||||
func init() {
|
||||
internal.GlobalRegistry.RegisterServer("database", &DBConfig{})
|
||||
common.GlobalRegistry.RegisterServer("database", &DBConfig{})
|
||||
}
|
||||
|
||||
type DBConfig struct {
|
||||
@@ -41,11 +41,11 @@ func (c *DBConfig) ParseConfig(config map[string]any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DBConfig) NewServer(serverName string) (*internal.MCPServer, error) {
|
||||
mcpServer := internal.NewMCPServer(
|
||||
func (c *DBConfig) NewServer(serverName string) (*common.MCPServer, error) {
|
||||
mcpServer := common.NewMCPServer(
|
||||
serverName,
|
||||
Version,
|
||||
internal.WithInstructions(fmt.Sprintf("This is a %s database server", c.dbType)),
|
||||
common.WithInstructions(fmt.Sprintf("This is a %s database server", c.dbType)),
|
||||
)
|
||||
|
||||
dbClient := NewDBClient(c.dsn, c.dbType, mcpServer.GetDestoryChannel())
|
||||
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// HandleQueryTool handles SQL query execution
|
||||
func HandleQueryTool(dbClient *DBClient) internal.ToolHandlerFunc {
|
||||
func HandleQueryTool(dbClient *DBClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
message, ok := arguments["sql"].(string)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package internal
|
||||
package common
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
@@ -1,4 +1,4 @@
|
||||
package internal
|
||||
package common
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
@@ -23,6 +23,27 @@ type MatchRule struct {
|
||||
MatchRuleType RuleType `json:"match_rule_type"` // Type of match rule
|
||||
}
|
||||
|
||||
// ParseMatchList parses the match list from the config
|
||||
func ParseMatchList(matchListConfig []interface{}) []MatchRule {
|
||||
matchList := make([]MatchRule, 0)
|
||||
for _, item := range matchListConfig {
|
||||
if ruleMap, ok := item.(map[string]interface{}); ok {
|
||||
rule := MatchRule{}
|
||||
if domain, ok := ruleMap["match_rule_domain"].(string); ok {
|
||||
rule.MatchRuleDomain = domain
|
||||
}
|
||||
if path, ok := ruleMap["match_rule_path"].(string); ok {
|
||||
rule.MatchRulePath = path
|
||||
}
|
||||
if ruleType, ok := ruleMap["match_rule_type"].(string); ok {
|
||||
rule.MatchRuleType = RuleType(ruleType)
|
||||
}
|
||||
matchList = append(matchList, rule)
|
||||
}
|
||||
}
|
||||
return matchList
|
||||
}
|
||||
|
||||
// convertWildcardToRegex converts wildcard pattern to regex pattern
|
||||
func convertWildcardToRegex(pattern string) string {
|
||||
pattern = regexp.QuoteMeta(pattern)
|
||||
@@ -87,3 +108,13 @@ func IsMatch(rules []MatchRule, host, path string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MatchDomainList checks if the domain matches any of the domains in the list
|
||||
func MatchDomainList(domain string, domainList []string) bool {
|
||||
for _, d := range domainList {
|
||||
if matchDomain(domain, d) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package internal
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -72,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())
|
||||
|
||||
@@ -83,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,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 {
|
||||
@@ -249,6 +250,18 @@ func (r *RedisClient) Get(key string) (string, error) {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Expire sets the expiration time for a key
|
||||
func (r *RedisClient) Expire(key string, expiration time.Duration) error {
|
||||
ok, err := r.client.Expire(r.ctx, key, expiration).Result()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set expiration for key: %w", err)
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("key does not exist")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the Redis client and stops the keepalive goroutine
|
||||
func (r *RedisClient) Close() error {
|
||||
r.cancel()
|
||||
@@ -1,4 +1,4 @@
|
||||
package internal
|
||||
package common
|
||||
|
||||
var GlobalRegistry = NewServerRegistry()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package internal
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -243,6 +243,7 @@ func (s *MCPServer) HandleMessage(
|
||||
message json.RawMessage,
|
||||
) mcp.JSONRPCMessage {
|
||||
// Add server to context
|
||||
|
||||
ctx = context.WithValue(ctx, serverKey{}, s)
|
||||
|
||||
var baseMessage struct {
|
||||
@@ -1,4 +1,4 @@
|
||||
package internal
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -210,15 +210,7 @@ func (s *SSEServer) HandleMessage(w http.ResponseWriter, r *http.Request, body j
|
||||
var status int
|
||||
// Only send response if there is one (not for notifications)
|
||||
if response != nil {
|
||||
eventData, _ := json.Marshal(response)
|
||||
|
||||
if sessionID != "" && s.redisClient != nil {
|
||||
channel := GetSSEChannelName(sessionID)
|
||||
publishErr := s.redisClient.Publish(channel, fmt.Sprintf("event: message\ndata: %s\n\n", eventData))
|
||||
|
||||
if publishErr != nil {
|
||||
api.LogErrorf("Failed to publish message to Redis: %v", publishErr)
|
||||
}
|
||||
if sessionID != ""{
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
status = http.StatusAccepted
|
||||
} else {
|
||||
34
plugins/golang-filter/mcp-session/common/utils.go
Normal file
34
plugins/golang-filter/mcp-session/common/utils.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
)
|
||||
|
||||
type RequestURL struct {
|
||||
Method string
|
||||
Scheme string
|
||||
Host string
|
||||
Path string
|
||||
BaseURL string
|
||||
ParsedURL *url.URL
|
||||
InternalIP bool
|
||||
}
|
||||
|
||||
func NewRequestURL(header api.RequestHeaderMap) *RequestURL {
|
||||
method, _ := header.Get(":method")
|
||||
scheme, _ := header.Get(":scheme")
|
||||
host, _ := header.Get(":authority")
|
||||
path, _ := header.Get(":path")
|
||||
internalIP, _ := header.Get("x-envoy-internal")
|
||||
baseURL := fmt.Sprintf("%s://%s", scheme, host)
|
||||
parsedURL, err := url.Parse(path)
|
||||
if err != nil {
|
||||
api.LogWarnf("url parse path:%s failed:%s", path, err)
|
||||
return nil
|
||||
}
|
||||
api.LogDebugf("RequestURL: method=%s, scheme=%s, host=%s, path=%s", method, scheme, host, path)
|
||||
return &RequestURL{Method: method, Scheme: scheme, Host: host, Path: path, BaseURL: baseURL, ParsedURL: parsedURL, InternalIP: internalIP == "true"}
|
||||
}
|
||||
145
plugins/golang-filter/mcp-session/config.go
Normal file
145
plugins/golang-filter/mcp-session/config.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package mcp_session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
_ "net/http/pprof"
|
||||
|
||||
xds "github.com/cncf/xds/go/xds/type/v3"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/handler"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
)
|
||||
|
||||
const Name = "mcp-session"
|
||||
const Version = "1.0.0"
|
||||
const ConfigPathSuffix = "/config"
|
||||
const DefaultServerName = "higress-mcp-server"
|
||||
|
||||
var GlobalSSEPathSuffix = "/sse"
|
||||
|
||||
type config struct {
|
||||
matchList []common.MatchRule
|
||||
enableUserLevelServer bool
|
||||
rateLimitConfig *handler.MCPRatelimitConfig
|
||||
defaultServer *common.SSEServer
|
||||
redisClient *common.RedisClient
|
||||
}
|
||||
|
||||
func (c *config) Destroy() {
|
||||
if c.redisClient != nil {
|
||||
api.LogDebug("Closing Redis client")
|
||||
c.redisClient.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type Parser struct {
|
||||
}
|
||||
|
||||
// Parse the filter configuration
|
||||
func (p *Parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (interface{}, error) {
|
||||
configStruct := &xds.TypedStruct{}
|
||||
if err := any.UnmarshalTo(configStruct); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v := configStruct.Value
|
||||
|
||||
conf := &config{
|
||||
matchList: make([]common.MatchRule, 0),
|
||||
}
|
||||
|
||||
// Parse match_list if exists
|
||||
if matchList, ok := v.AsMap()["match_list"].([]interface{}); ok {
|
||||
conf.matchList = common.ParseMatchList(matchList)
|
||||
}
|
||||
|
||||
// Redis configuration is optional
|
||||
if redisConfigMap, ok := v.AsMap()["redis"].(map[string]interface{}); ok {
|
||||
redisConfig, err := common.ParseRedisConfig(redisConfigMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse redis config: %w", err)
|
||||
}
|
||||
|
||||
redisClient, err := common.NewRedisClient(redisConfig)
|
||||
if err != nil {
|
||||
api.LogErrorf("Failed to initialize Redis client: %w", err)
|
||||
} else {
|
||||
api.LogDebug("Redis client initialized")
|
||||
}
|
||||
conf.redisClient = redisClient
|
||||
} else {
|
||||
api.LogDebug("Redis configuration not provided, running without Redis")
|
||||
}
|
||||
|
||||
enableUserLevelServer, ok := v.AsMap()["enable_user_level_server"].(bool)
|
||||
if !ok {
|
||||
enableUserLevelServer = false
|
||||
if conf.redisClient == nil {
|
||||
return nil, fmt.Errorf("redis configuration is not provided, enable_user_level_server is true")
|
||||
}
|
||||
}
|
||||
conf.enableUserLevelServer = enableUserLevelServer
|
||||
|
||||
if rateLimit, ok := v.AsMap()["rate_limit"].(map[string]interface{}); ok {
|
||||
rateLimitConfig := &handler.MCPRatelimitConfig{}
|
||||
if limit, ok := rateLimit["limit"].(float64); ok {
|
||||
rateLimitConfig.Limit = int(limit)
|
||||
}
|
||||
if window, ok := rateLimit["window"].(float64); ok {
|
||||
rateLimitConfig.Window = int(window)
|
||||
}
|
||||
if whiteList, ok := rateLimit["white_list"].([]interface{}); ok {
|
||||
for _, item := range whiteList {
|
||||
if uid, ok := item.(string); ok {
|
||||
rateLimitConfig.Whitelist = append(rateLimitConfig.Whitelist, uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
if errorText, ok := rateLimit["error_text"].(string); ok {
|
||||
rateLimitConfig.ErrorText = errorText
|
||||
}
|
||||
conf.rateLimitConfig = rateLimitConfig
|
||||
}
|
||||
|
||||
ssePathSuffix, ok := v.AsMap()["sse_path_suffix"].(string)
|
||||
if !ok || ssePathSuffix == "" {
|
||||
return nil, fmt.Errorf("sse path suffix is not set or empty")
|
||||
}
|
||||
GlobalSSEPathSuffix = ssePathSuffix
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func (p *Parser) Merge(parent interface{}, child interface{}) interface{} {
|
||||
parentConfig := parent.(*config)
|
||||
childConfig := child.(*config)
|
||||
|
||||
newConfig := *parentConfig
|
||||
if childConfig.matchList != nil {
|
||||
newConfig.matchList = childConfig.matchList
|
||||
}
|
||||
newConfig.enableUserLevelServer = childConfig.enableUserLevelServer
|
||||
if childConfig.rateLimitConfig != nil {
|
||||
newConfig.rateLimitConfig = childConfig.rateLimitConfig
|
||||
}
|
||||
if childConfig.defaultServer != nil {
|
||||
newConfig.defaultServer = childConfig.defaultServer
|
||||
}
|
||||
return &newConfig
|
||||
}
|
||||
|
||||
func FilterFactory(c interface{}, callbacks api.FilterCallbackHandler) api.StreamFilter {
|
||||
conf, ok := c.(*config)
|
||||
if !ok {
|
||||
panic("unexpected config type")
|
||||
}
|
||||
return &filter{
|
||||
callbacks: callbacks,
|
||||
config: conf,
|
||||
stopChan: make(chan struct{}),
|
||||
mcpConfigHandler: handler.NewMCPConfigHandler(conf.redisClient, callbacks),
|
||||
mcpRatelimitHandler: handler.NewMCPRatelimitHandler(conf.redisClient, callbacks, conf.rateLimitConfig),
|
||||
}
|
||||
}
|
||||
240
plugins/golang-filter/mcp-session/filter.go
Normal file
240
plugins/golang-filter/mcp-session/filter.go
Normal file
@@ -0,0 +1,240 @@
|
||||
package mcp_session
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/handler"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
const (
|
||||
RedisNotEnabledResponseBody = "Redis is not enabled, SSE connection is not supported"
|
||||
)
|
||||
|
||||
// The callbacks in the filter, like `DecodeHeaders`, can be implemented on demand.
|
||||
// Because api.PassThroughStreamFilter provides a default implementation.
|
||||
type filter struct {
|
||||
api.PassThroughStreamFilter
|
||||
|
||||
callbacks api.FilterCallbackHandler
|
||||
path string
|
||||
config *config
|
||||
stopChan chan struct{}
|
||||
|
||||
req *http.Request
|
||||
serverName string
|
||||
proxyURL *url.URL
|
||||
neepProcess bool
|
||||
|
||||
userLevelConfig bool
|
||||
mcpConfigHandler *handler.MCPConfigHandler
|
||||
ratelimit bool
|
||||
mcpRatelimitHandler *handler.MCPRatelimitHandler
|
||||
}
|
||||
|
||||
// Callbacks which are called in request path
|
||||
// The endStream is true if the request doesn't have body
|
||||
func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.StatusType {
|
||||
url := common.NewRequestURL(header)
|
||||
if url == nil {
|
||||
return api.Continue
|
||||
}
|
||||
f.path = url.ParsedURL.Path
|
||||
|
||||
// Check if request matches any rule in match_list
|
||||
if !common.IsMatch(f.config.matchList, url.Host, f.path) {
|
||||
api.LogDebugf("Request does not match any rule in match_list: %s", url.ParsedURL.String())
|
||||
return api.Continue
|
||||
}
|
||||
f.neepProcess = true
|
||||
|
||||
f.req = &http.Request{
|
||||
Method: url.Method,
|
||||
URL: url.ParsedURL,
|
||||
}
|
||||
|
||||
if strings.HasSuffix(f.path, ConfigPathSuffix) && f.config.enableUserLevelServer {
|
||||
if !url.InternalIP {
|
||||
api.LogWarnf("Access denied: non-Internal IP address %s", url.ParsedURL.String())
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
if strings.HasSuffix(f.path, ConfigPathSuffix) && url.Method == http.MethodGet {
|
||||
api.LogDebugf("Handling config request: %s", f.path)
|
||||
f.mcpConfigHandler.HandleConfigRequest(f.req, []byte{})
|
||||
return api.LocalReply
|
||||
}
|
||||
f.userLevelConfig = true
|
||||
if endStream {
|
||||
return api.Continue
|
||||
} else {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(url.ParsedURL.Path, GlobalSSEPathSuffix) {
|
||||
f.proxyURL = url.ParsedURL
|
||||
if f.config.enableUserLevelServer {
|
||||
parts := strings.Split(url.ParsedURL.Path, "/")
|
||||
if len(parts) >= 3 {
|
||||
serverName := parts[1]
|
||||
uid := parts[2]
|
||||
// Get encoded config
|
||||
encodedConfig, _ := f.mcpConfigHandler.GetEncodedConfig(serverName, uid)
|
||||
if encodedConfig != "" {
|
||||
header.Set("x-higress-mcpserver-config", encodedConfig)
|
||||
api.LogDebugf("Set x-higress-mcpserver-config Header for %s:%s", serverName, uid)
|
||||
}
|
||||
}
|
||||
f.ratelimit = true
|
||||
}
|
||||
if endStream {
|
||||
return api.Continue
|
||||
} else {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
}
|
||||
|
||||
if url.Method != http.MethodGet {
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
|
||||
} else {
|
||||
f.config.defaultServer = common.NewSSEServer(common.NewMCPServer(DefaultServerName, Version),
|
||||
common.WithSSEEndpoint(GlobalSSEPathSuffix),
|
||||
common.WithMessageEndpoint(strings.TrimSuffix(url.ParsedURL.Path, GlobalSSEPathSuffix)),
|
||||
common.WithRedisClient(f.config.redisClient))
|
||||
f.serverName = f.config.defaultServer.GetServerName()
|
||||
body := "SSE connection create"
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusOK, body, nil, 0, "")
|
||||
}
|
||||
return api.LocalReply
|
||||
}
|
||||
|
||||
// DecodeData might be called multiple times during handling the request body.
|
||||
// The endStream is true when handling the last piece of the body.
|
||||
func (f *filter) DecodeData(buffer api.BufferInstance, endStream bool) api.StatusType {
|
||||
if !f.neepProcess {
|
||||
return api.Continue
|
||||
}
|
||||
if !endStream {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
if f.userLevelConfig {
|
||||
// Handle config POST request
|
||||
api.LogDebugf("Handling config request: %s", f.path)
|
||||
f.mcpConfigHandler.HandleConfigRequest(f.req, buffer.Bytes())
|
||||
return api.LocalReply
|
||||
} else if f.ratelimit {
|
||||
if checkJSONRPCMethod(buffer.Bytes(), "tools/list") {
|
||||
api.LogDebugf("Not a tools call request, skipping ratelimit")
|
||||
return api.Continue
|
||||
}
|
||||
parts := strings.Split(f.req.URL.Path, "/")
|
||||
if len(parts) < 3 {
|
||||
api.LogWarnf("Access denied: no valid uid found")
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
serverName := parts[1]
|
||||
uid := parts[2]
|
||||
encodedConfig, err := f.mcpConfigHandler.GetEncodedConfig(serverName, uid)
|
||||
if err != nil {
|
||||
api.LogWarnf("Access denied: no valid config found for uid %s", uid)
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return api.LocalReply
|
||||
} else if encodedConfig == "" && checkJSONRPCMethod(buffer.Bytes(), "tools/call") {
|
||||
api.LogDebugf("Empty config found for %s:%s", serverName, uid)
|
||||
if !f.mcpRatelimitHandler.HandleRatelimit(f.req, buffer.Bytes()) {
|
||||
return api.LocalReply
|
||||
}
|
||||
}
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
// Callbacks which are called in response path
|
||||
// The endStream is true if the response doesn't have body
|
||||
func (f *filter) EncodeHeaders(header api.ResponseHeaderMap, endStream bool) api.StatusType {
|
||||
if !f.neepProcess {
|
||||
return api.Continue
|
||||
}
|
||||
if f.serverName != "" {
|
||||
if f.config.redisClient != nil {
|
||||
header.Set("Content-Type", "text/event-stream")
|
||||
header.Set("Cache-Control", "no-cache")
|
||||
header.Set("Connection", "keep-alive")
|
||||
header.Set("Access-Control-Allow-Origin", "*")
|
||||
header.Del("Content-Length")
|
||||
} else {
|
||||
header.Set("Content-Length", strconv.Itoa(len(RedisNotEnabledResponseBody)))
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
// EncodeData might be called multiple times during handling the response body.
|
||||
// The endStream is true when handling the last piece of the body.
|
||||
func (f *filter) EncodeData(buffer api.BufferInstance, endStream bool) api.StatusType {
|
||||
if !f.neepProcess {
|
||||
return api.Continue
|
||||
}
|
||||
if !endStream {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
if f.proxyURL != nil && f.config.redisClient != nil {
|
||||
sessionID := f.proxyURL.Query().Get("sessionId")
|
||||
if sessionID != "" {
|
||||
channel := common.GetSSEChannelName(sessionID)
|
||||
eventData := fmt.Sprintf("event: message\ndata: %s\n\n", buffer.String())
|
||||
publishErr := f.config.redisClient.Publish(channel, eventData)
|
||||
if publishErr != nil {
|
||||
api.LogErrorf("Failed to publish wasm mcp server message to Redis: %v", publishErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if f.serverName != "" {
|
||||
if f.config.redisClient != nil {
|
||||
// handle default server
|
||||
buffer.Reset()
|
||||
f.config.defaultServer.HandleSSE(f.callbacks, f.stopChan)
|
||||
return api.Running
|
||||
} else {
|
||||
buffer.SetString(RedisNotEnabledResponseBody)
|
||||
return api.Continue
|
||||
}
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
// OnDestroy stops the goroutine
|
||||
func (f *filter) OnDestroy(reason api.DestroyReason) {
|
||||
api.LogDebugf("OnDestroy: reason=%v", reason)
|
||||
if f.serverName != "" && f.stopChan != nil {
|
||||
select {
|
||||
case <-f.stopChan:
|
||||
return
|
||||
default:
|
||||
api.LogDebug("Stopping SSE connection")
|
||||
close(f.stopChan)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if the request is a tools/call request
|
||||
func checkJSONRPCMethod(body []byte, method string) bool {
|
||||
var request mcp.CallToolRequest
|
||||
if err := json.Unmarshal(body, &request); err != nil {
|
||||
api.LogWarnf("Failed to unmarshal request body: %v, not a JSON RPC request", err)
|
||||
return true
|
||||
}
|
||||
|
||||
return request.Method == method
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@ type MCPConfigHandler struct {
|
||||
}
|
||||
|
||||
// NewMCPConfigHandler creates a new instance of MCP configuration handler
|
||||
func NewMCPConfigHandler(redisClient *internal.RedisClient, callbacks api.FilterCallbackHandler) *MCPConfigHandler {
|
||||
func NewMCPConfigHandler(redisClient *common.RedisClient, callbacks api.FilterCallbackHandler) *MCPConfigHandler {
|
||||
return &MCPConfigHandler{
|
||||
configStore: NewRedisConfigStore(redisClient),
|
||||
callbacks: callbacks,
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -36,11 +36,11 @@ type ConfigStore interface {
|
||||
|
||||
// RedisConfigStore implements configuration storage using Redis
|
||||
type RedisConfigStore struct {
|
||||
redisClient *internal.RedisClient
|
||||
redisClient *common.RedisClient
|
||||
}
|
||||
|
||||
// NewRedisConfigStore creates a new instance of Redis configuration storage
|
||||
func NewRedisConfigStore(redisClient *internal.RedisClient) ConfigStore {
|
||||
func NewRedisConfigStore(redisClient *common.RedisClient) ConfigStore {
|
||||
return &RedisConfigStore{
|
||||
redisClient: redisClient,
|
||||
}
|
||||
@@ -101,5 +101,11 @@ func (s *RedisConfigStore) GetConfig(serverName string, uid string) (map[string]
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Refresh TTL
|
||||
if err := s.redisClient.Expire(key, configExpiry); err != nil {
|
||||
// Log error but don't fail the request
|
||||
fmt.Printf("Failed to refresh TTL for key %s: %v\n", key, err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
@@ -8,13 +8,13 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
type MCPRatelimitHandler struct {
|
||||
redisClient *internal.RedisClient
|
||||
redisClient *common.RedisClient
|
||||
callbacks api.FilterCallbackHandler
|
||||
limit int // Maximum requests allowed per window
|
||||
window int // Time window in seconds
|
||||
@@ -31,7 +31,7 @@ type MCPRatelimitConfig struct {
|
||||
}
|
||||
|
||||
// NewMCPRatelimitHandler creates a new rate limit handler
|
||||
func NewMCPRatelimitHandler(redisClient *internal.RedisClient, callbacks api.FilterCallbackHandler, conf *MCPRatelimitConfig) *MCPRatelimitHandler {
|
||||
func NewMCPRatelimitHandler(redisClient *common.RedisClient, callbacks api.FilterCallbackHandler, conf *MCPRatelimitConfig) *MCPRatelimitHandler {
|
||||
if conf == nil {
|
||||
conf = &MCPRatelimitConfig{
|
||||
Limit: 100,
|
||||
@@ -55,6 +55,7 @@ constexpr std::string_view Host(":authority");
|
||||
constexpr std::string_view Path(":path");
|
||||
constexpr std::string_view EnvoyOriginalPath("x-envoy-original-path");
|
||||
constexpr std::string_view Accept("accept");
|
||||
constexpr std::string_view ContentDisposition("content-disposition");
|
||||
constexpr std::string_view ContentMD5("content-md5");
|
||||
constexpr std::string_view ContentType("content-type");
|
||||
constexpr std::string_view ContentLength("content-length");
|
||||
@@ -68,6 +69,7 @@ constexpr std::string_view StrictTransportSecurity("strict-transport-security");
|
||||
namespace ContentTypeValues {
|
||||
constexpr std::string_view Grpc{"application/grpc"};
|
||||
constexpr std::string_view Json{"application/json"};
|
||||
constexpr std::string_view MultipartFormData{"multipart/form-data"};
|
||||
} // namespace ContentTypeValues
|
||||
|
||||
class PercentEncoding {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <limits>
|
||||
#include <regex>
|
||||
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
@@ -123,6 +124,7 @@ bool PluginRootContext::configure(size_t configuration_size) {
|
||||
}
|
||||
|
||||
FilterHeadersStatus PluginRootContext::onHeader(
|
||||
PluginContext& ctx,
|
||||
const ModelRouterConfigRule& rule) {
|
||||
if (!Wasm::Common::Http::hasRequestBody()) {
|
||||
return FilterHeadersStatus::Continue;
|
||||
@@ -150,19 +152,49 @@ FilterHeadersStatus PluginRootContext::onHeader(
|
||||
if (!enable) {
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
auto content_type_value =
|
||||
auto content_type_ptr =
|
||||
getRequestHeader(Wasm::Common::Http::Header::ContentType);
|
||||
if (!absl::StrContains(content_type_value->view(),
|
||||
auto content_type_value = content_type_ptr->view();
|
||||
LOG_DEBUG(absl::StrCat("Content-Type: ", content_type_value));
|
||||
if (absl::StrContains(content_type_value,
|
||||
Wasm::Common::Http::ContentTypeValues::Json)) {
|
||||
return FilterHeadersStatus::Continue;
|
||||
ctx.mode_ = MODE_JSON;
|
||||
LOG_DEBUG("Enable JSON mode.");
|
||||
removeRequestHeader(Wasm::Common::Http::Header::ContentLength);
|
||||
setFilterState(SetDecoderBufferLimitKey, DefaultMaxBodyBytes);
|
||||
LOG_INFO(absl::StrCat("SetRequestBodyBufferLimit: ", DefaultMaxBodyBytes));
|
||||
return FilterHeadersStatus::StopIteration;
|
||||
}
|
||||
removeRequestHeader(Wasm::Common::Http::Header::ContentLength);
|
||||
setFilterState(SetDecoderBufferLimitKey, DefaultMaxBodyBytes);
|
||||
LOG_INFO(absl::StrCat("SetRequestBodyBufferLimit: ", DefaultMaxBodyBytes));
|
||||
return FilterHeadersStatus::StopIteration;
|
||||
if (absl::StrContains(content_type_value,
|
||||
Wasm::Common::Http::ContentTypeValues::MultipartFormData)) {
|
||||
// Get the boundary from the content type
|
||||
auto boundary_start = content_type_value.find("boundary=");
|
||||
if (boundary_start == std::string::npos) {
|
||||
LOG_WARN(absl::StrCat("No boundary found in a multipart/form-data content-type: ", content_type_value));
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
boundary_start += 9;
|
||||
auto boundary_end = content_type_value.find(';', boundary_start);
|
||||
if (boundary_end == std::string::npos) {
|
||||
boundary_end = content_type_value.size();
|
||||
}
|
||||
auto boundary_length = boundary_end - boundary_start;
|
||||
if (boundary_length < 1 || boundary_length > 70) {
|
||||
// See https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
|
||||
LOG_WARN(absl::StrCat("Invalid boundary value in a multipart/form-data content-type: ", content_type_value));
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
auto boundary_value = content_type_value.substr(boundary_start, boundary_end - boundary_start);
|
||||
ctx.mode_ = MODE_MULTIPART;
|
||||
ctx.boundary_ = boundary_value;
|
||||
LOG_DEBUG(absl::StrCat("Enable multipart/form-data mode. Boundary=", boundary_value));
|
||||
removeRequestHeader(Wasm::Common::Http::Header::ContentLength);
|
||||
return FilterHeadersStatus::StopIteration;
|
||||
}
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
|
||||
FilterDataStatus PluginRootContext::onBody(const ModelRouterConfigRule& rule,
|
||||
FilterDataStatus PluginRootContext::onJsonBody(const ModelRouterConfigRule& rule,
|
||||
std::string_view body) {
|
||||
const auto& model_key = rule.model_key_;
|
||||
const auto& add_provider_header = rule.add_provider_header_;
|
||||
@@ -198,10 +230,85 @@ FilterDataStatus PluginRootContext::onBody(const ModelRouterConfigRule& rule,
|
||||
return FilterDataStatus::Continue;
|
||||
}
|
||||
|
||||
FilterDataStatus PluginRootContext::onMultipartBody(
|
||||
PluginContext& ctx,
|
||||
const ModelRouterConfigRule& rule,
|
||||
WasmDataPtr& body,
|
||||
bool end_stream) {
|
||||
const auto& add_provider_header = rule.add_provider_header_;
|
||||
const auto& model_to_header = rule.model_to_header_;
|
||||
|
||||
const auto boundary = ctx.boundary_;
|
||||
const auto body_view = body->view();
|
||||
const auto model_param_header = absl::StrCat("Content-Disposition: form-data; name=\"", rule.model_key_, "\"");
|
||||
|
||||
for (size_t pos = 0; (pos = body_view.find(boundary, pos)) != std::string_view::npos;) {
|
||||
LOG_DEBUG(absl::StrCat("Found boundary at ", pos));
|
||||
pos += boundary.length();
|
||||
size_t end_pos = body_view.find(boundary, pos);
|
||||
if (end_pos == std::string_view::npos) {
|
||||
end_pos = body_view.length();
|
||||
}
|
||||
std::string_view part = body_view.substr(pos, end_pos - pos);
|
||||
LOG_DEBUG(absl::StrCat("Part: ", part));
|
||||
auto part_pos = pos;
|
||||
pos = end_pos;
|
||||
|
||||
// Check if this part contains the model parameter
|
||||
if (!absl::StrContains(part, model_param_header)) {
|
||||
LOG_DEBUG("Part does not contain model parameter");
|
||||
continue;
|
||||
}
|
||||
size_t value_start = part.find(CRLF_CRLF);
|
||||
if (value_start == std::string_view::npos) {
|
||||
LOG_DEBUG("No value start found in part");
|
||||
break;
|
||||
}
|
||||
value_start += 4; // Skip the "\r\n\r\n"
|
||||
// model parameter should be only one line
|
||||
size_t value_end = part.find(CRLF, value_start);
|
||||
if (value_end == std::string_view::npos) {
|
||||
LOG_DEBUG("No value end found in part");
|
||||
break;
|
||||
}
|
||||
auto model_value = part.substr(value_start, value_end - value_start);
|
||||
LOG_DEBUG(absl::StrCat("Model value: ", model_value));
|
||||
if (!model_to_header.empty()) {
|
||||
replaceRequestHeader(model_to_header, model_value);
|
||||
}
|
||||
if (!add_provider_header.empty()) {
|
||||
auto pos = model_value.find('/');
|
||||
if (pos != std::string::npos) {
|
||||
const auto& provider = model_value.substr(0, pos);
|
||||
const auto& model = model_value.substr(pos + 1);
|
||||
replaceRequestHeader(add_provider_header, provider);
|
||||
size_t new_size = 0;
|
||||
auto new_buffer_data = absl::StrCat(body_view.substr(0, part_pos + value_start), model, body_view.substr(part_pos + value_end));
|
||||
auto result = setBuffer(WasmBufferType::HttpRequestBody, 0, std::numeric_limits<size_t>::max(), new_buffer_data, &new_size);
|
||||
LOG_DEBUG(absl::StrCat("model route to provider:", provider,
|
||||
", model:", model));
|
||||
LOG_DEBUG(absl::StrCat("result=", result, " new_size=", new_size));
|
||||
} else {
|
||||
LOG_DEBUG(absl::StrCat("model route to provider not work, model:",
|
||||
model_value));
|
||||
}
|
||||
}
|
||||
// We are done now. We can stop processing the body.
|
||||
LOG_DEBUG(absl::StrCat("Done processing multipart body after caching ", body_view.length() , " bytes."));
|
||||
ctx.mode_ = MODE_BYPASS;
|
||||
return FilterDataStatus::Continue;
|
||||
}
|
||||
if (end_stream) {
|
||||
LOG_DEBUG("No model parameter found in the body");
|
||||
return FilterDataStatus::Continue;
|
||||
}
|
||||
return FilterDataStatus::StopIterationAndBuffer;
|
||||
}
|
||||
|
||||
FilterHeadersStatus PluginContext::onRequestHeaders(uint32_t, bool) {
|
||||
auto* rootCtx = rootContext();
|
||||
return rootCtx->onHeaders([rootCtx, this](const auto& config) {
|
||||
auto ret = rootCtx->onHeader(config);
|
||||
auto ret = rootCtx->onHeader(*this, config);
|
||||
if (ret == FilterHeadersStatus::StopIteration) {
|
||||
this->config_ = &config;
|
||||
}
|
||||
@@ -214,14 +321,28 @@ FilterDataStatus PluginContext::onRequestBody(size_t body_size,
|
||||
if (config_ == nullptr) {
|
||||
return FilterDataStatus::Continue;
|
||||
}
|
||||
body_total_size_ += body_size;
|
||||
if (!end_stream) {
|
||||
return FilterDataStatus::StopIterationAndBuffer;
|
||||
}
|
||||
auto body =
|
||||
getBufferBytes(WasmBufferType::HttpRequestBody, 0, body_total_size_);
|
||||
auto* rootCtx = rootContext();
|
||||
return rootCtx->onBody(*config_, body->view());
|
||||
body_total_size_ += body_size;
|
||||
switch (mode_) {
|
||||
case MODE_JSON:
|
||||
{
|
||||
if (!end_stream) {
|
||||
return FilterDataStatus::StopIterationAndBuffer;
|
||||
}
|
||||
auto body =
|
||||
getBufferBytes(WasmBufferType::HttpRequestBody, 0, body_total_size_);
|
||||
return rootCtx->onJsonBody(*config_, body->view());
|
||||
}
|
||||
case MODE_MULTIPART:
|
||||
{
|
||||
auto body =
|
||||
getBufferBytes(WasmBufferType::HttpRequestBody, 0, body_total_size_);
|
||||
return rootCtx->onMultipartBody(*this, *config_, body, end_stream);
|
||||
}
|
||||
case MODE_BYPASS:
|
||||
default:
|
||||
return FilterDataStatus::Continue;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
@@ -36,6 +36,13 @@ namespace model_router {
|
||||
|
||||
#endif
|
||||
|
||||
#define MODE_BYPASS 0
|
||||
#define MODE_JSON 1
|
||||
#define MODE_MULTIPART 2
|
||||
|
||||
#define CRLF ("\r\n")
|
||||
#define CRLF_CRLF ("\r\n\r\n")
|
||||
|
||||
struct ModelRouterConfigRule {
|
||||
std::string model_key_ = "model";
|
||||
std::string add_provider_header_;
|
||||
@@ -45,6 +52,8 @@ struct ModelRouterConfigRule {
|
||||
"/audio/speech", "/fine_tuning/jobs", "/moderations"};
|
||||
};
|
||||
|
||||
class PluginContext;
|
||||
|
||||
// PluginRootContext is the root context for all streams processed by the
|
||||
// thread. It has the same lifetime as the worker thread and acts as target for
|
||||
// interactions that outlives individual stream, e.g. timer, async calls.
|
||||
@@ -55,8 +64,9 @@ class PluginRootContext : public RootContext,
|
||||
: RootContext(id, root_id) {}
|
||||
~PluginRootContext() {}
|
||||
bool onConfigure(size_t) override;
|
||||
FilterHeadersStatus onHeader(const ModelRouterConfigRule&);
|
||||
FilterDataStatus onBody(const ModelRouterConfigRule&, std::string_view);
|
||||
FilterHeadersStatus onHeader(PluginContext& ctx, const ModelRouterConfigRule&);
|
||||
FilterDataStatus onJsonBody(const ModelRouterConfigRule&, std::string_view);
|
||||
FilterDataStatus onMultipartBody(PluginContext& ctx, const ModelRouterConfigRule& rule, WasmDataPtr& body, bool end_stream);
|
||||
bool configure(size_t);
|
||||
|
||||
private:
|
||||
@@ -69,6 +79,8 @@ class PluginContext : public Context {
|
||||
explicit PluginContext(uint32_t id, RootContext* root) : Context(id, root) {}
|
||||
FilterHeadersStatus onRequestHeaders(uint32_t, bool) override;
|
||||
FilterDataStatus onRequestBody(size_t, bool) override;
|
||||
int mode_;
|
||||
std::string boundary_;
|
||||
|
||||
private:
|
||||
inline PluginRootContext* rootContext() {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "extensions/model_router/plugin.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <regex>
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
@@ -86,7 +87,7 @@ class ModelRouterTest : public ::testing::Test {
|
||||
.WillByDefault([&](WasmHeaderMapType, std::string_view header,
|
||||
std::string_view* result) {
|
||||
if (header == "content-type") {
|
||||
*result = "application/json";
|
||||
*result = content_type_;
|
||||
} else if (header == "content-length") {
|
||||
*result = "1024";
|
||||
} else if (header == ":path") {
|
||||
@@ -125,6 +126,7 @@ class ModelRouterTest : public ::testing::Test {
|
||||
std::unique_ptr<PluginContext> context_;
|
||||
std::string route_name_;
|
||||
std::string path_;
|
||||
std::string content_type_ = "application/json";
|
||||
BufferBase body_;
|
||||
BufferBase config_;
|
||||
};
|
||||
@@ -133,7 +135,7 @@ TEST_F(ModelRouterTest, RewriteModelAndHeader) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"addProviderHeader": "x-higress-llm-provider"
|
||||
})";
|
||||
})";
|
||||
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
@@ -155,14 +157,14 @@ TEST_F(ModelRouterTest, RewriteModelAndHeader) {
|
||||
body_.set(request_json);
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
EXPECT_EQ(context_->onRequestBody(28, true), FilterDataStatus::Continue);
|
||||
EXPECT_EQ(context_->onRequestBody(request_json.length(), true), FilterDataStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(ModelRouterTest, ModelToHeader) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"modelToHeader": "x-higress-llm-model"
|
||||
})";
|
||||
})";
|
||||
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
@@ -181,14 +183,14 @@ TEST_F(ModelRouterTest, ModelToHeader) {
|
||||
body_.set(request_json);
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
EXPECT_EQ(context_->onRequestBody(28, true), FilterDataStatus::Continue);
|
||||
EXPECT_EQ(context_->onRequestBody(request_json.length(), true), FilterDataStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(ModelRouterTest, IgnorePath) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"addProviderHeader": "x-higress-llm-provider"
|
||||
})";
|
||||
})";
|
||||
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
@@ -208,7 +210,7 @@ TEST_F(ModelRouterTest, IgnorePath) {
|
||||
body_.set(request_json);
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
EXPECT_EQ(context_->onRequestBody(28, true), FilterDataStatus::Continue);
|
||||
EXPECT_EQ(context_->onRequestBody(request_json.length(), true), FilterDataStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(ModelRouterTest, RouteLevelRewriteModelAndHeader) {
|
||||
@@ -242,7 +244,178 @@ TEST_F(ModelRouterTest, RouteLevelRewriteModelAndHeader) {
|
||||
route_name_ = "route-a";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
EXPECT_EQ(context_->onRequestBody(28, true), FilterDataStatus::Continue);
|
||||
EXPECT_EQ(context_->onRequestBody(request_json.length(), true), FilterDataStatus::Continue);
|
||||
}
|
||||
|
||||
|
||||
TEST_F(ModelRouterTest, RewriteModelAndHeaderMultipartFormData) {
|
||||
std::string configuration = R"({
|
||||
"addProviderHeader": "x-higress-llm-provider"
|
||||
})";
|
||||
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
path_ = "/v1/chat/completions";
|
||||
content_type_ = "multipart/form-data; boundary=--------------------------100751621174704322650451";
|
||||
std::string request_data = std::regex_replace(R"(
|
||||
----------------------------100751621174704322650451
|
||||
Content-Disposition: form-data; name="purpose"
|
||||
|
||||
batch
|
||||
----------------------------100751621174704322650451
|
||||
Content-Disposition: form-data; name="model"
|
||||
|
||||
qwen/qwen-turbo
|
||||
----------------------------100751621174704322650451
|
||||
Content-Disposition: form-data; name="file"; filename="test-data.json"
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
]
|
||||
----------------------------100751621174704322650451--
|
||||
)", std::regex("\n"), "\r\n"); // Multipart data requires CRLF line endings
|
||||
EXPECT_CALL(*mock_context_,
|
||||
setBuffer(testing::_, testing::_, testing::_, testing::_))
|
||||
.WillOnce([&](WasmBufferType, size_t start, size_t length, std::string_view body) {
|
||||
std::cerr << "===============" << "\n";
|
||||
std::cerr << body << "\n";
|
||||
std::cerr << "===============" << "\n";
|
||||
EXPECT_EQ(start, 0);
|
||||
EXPECT_EQ(length, std::numeric_limits<size_t>::max());
|
||||
auto expected_body= std::regex_replace(R"(
|
||||
----------------------------100751621174704322650451
|
||||
Content-Disposition: form-data; name="purpose"
|
||||
|
||||
batch
|
||||
----------------------------100751621174704322650451
|
||||
Content-Disposition: form-data; name="model"
|
||||
|
||||
qwen-turbo
|
||||
)", std::regex("\n"), "\r\n"); // Multipart data requires CRLF line endings
|
||||
EXPECT_EQ(body, expected_body);
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
EXPECT_CALL(*mock_context_,
|
||||
replaceHeaderMapValue(testing::_,
|
||||
std::string_view("x-higress-llm-provider"),
|
||||
std::string_view("qwen")));
|
||||
|
||||
body_.set(request_data);
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
auto last_body_size = 0;
|
||||
|
||||
auto body = request_data.substr(0, request_data.find("batch") + 5 + 2 /* batch + CRLF */);
|
||||
body_.set(body);
|
||||
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::StopIterationAndBuffer);
|
||||
last_body_size = body.size();
|
||||
|
||||
body = request_data.substr(0, request_data.find("\"model\"") + 5 + 2 + 2 /* "model" + CRLF + CRLF */);
|
||||
body_.set(body);
|
||||
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::StopIterationAndBuffer);
|
||||
last_body_size = body.size();
|
||||
|
||||
body = request_data.substr(0, request_data.find("qwen") + 4 /* "qwen" */);
|
||||
body_.set(body);
|
||||
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::StopIterationAndBuffer);
|
||||
last_body_size = body.size();
|
||||
|
||||
body = request_data.substr(0, request_data.find("qwen-turbo") + 10 /* "qwen-turbo" */);
|
||||
body_.set(body);
|
||||
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::StopIterationAndBuffer);
|
||||
last_body_size = body.size();
|
||||
|
||||
body = request_data.substr(0, request_data.find("qwen-turbo") + 10 + 2 /* "qwen-turbo" + CRLF */);
|
||||
body_.set(body);
|
||||
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::Continue);
|
||||
last_body_size = body.size();
|
||||
|
||||
body = request_data.substr(0, request_data.find("qwen-turbo") + 10 + 2 + 50 /* "qwen-turbo" + CRLF + boundary */);
|
||||
body_.set(body);
|
||||
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::Continue);
|
||||
last_body_size = body.size();
|
||||
|
||||
body_.set(request_data);
|
||||
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, true), FilterDataStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(ModelRouterTest, ModelToHeaderMultipartFormData) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"modelToHeader": "x-higress-llm-model"
|
||||
})";
|
||||
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
path_ = "/v1/chat/completions";
|
||||
content_type_ = "multipart/form-data; boundary=--------------------------100751621174704322650451";
|
||||
std::string request_data = std::regex_replace(R"(
|
||||
----------------------------100751621174704322650451
|
||||
Content-Disposition: form-data; name="purpose"
|
||||
|
||||
batch
|
||||
----------------------------100751621174704322650451
|
||||
Content-Disposition: form-data; name="model"
|
||||
|
||||
qwen-max
|
||||
----------------------------100751621174704322650451
|
||||
Content-Disposition: form-data; name="file"; filename="test-data.json"
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
]
|
||||
----------------------------100751621174704322650451--
|
||||
)", std::regex("\n"), "\r\n"); // Multipart data requires CRLF line endings
|
||||
EXPECT_CALL(*mock_context_,
|
||||
setBuffer(testing::_, testing::_, testing::_, testing::_))
|
||||
.Times(0);
|
||||
|
||||
EXPECT_CALL(
|
||||
*mock_context_,
|
||||
replaceHeaderMapValue(testing::_, std::string_view("x-higress-llm-model"),
|
||||
std::string_view("qwen-max")));
|
||||
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
auto last_body_size = 0;
|
||||
|
||||
auto body = request_data.substr(0, request_data.find("batch") + 5 + 2 /* batch + CRLF */);
|
||||
body_.set(body);
|
||||
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::StopIterationAndBuffer);
|
||||
last_body_size = body.size();
|
||||
|
||||
body = request_data.substr(0, request_data.find("\"model\"") + 5 + 2 + 2 /* "model" + CRLF + CRLF */);
|
||||
body_.set(body);
|
||||
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::StopIterationAndBuffer);
|
||||
last_body_size = body.size();
|
||||
|
||||
body = request_data.substr(0, request_data.find("qwen") + 4 /* "qwen" */);
|
||||
body_.set(body);
|
||||
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::StopIterationAndBuffer);
|
||||
last_body_size = body.size();
|
||||
|
||||
body = request_data.substr(0, request_data.find("qwen-max") + 8 /* "qwen-max" */);
|
||||
body_.set(body);
|
||||
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::StopIterationAndBuffer);
|
||||
last_body_size = body.size();
|
||||
|
||||
body = request_data.substr(0, request_data.find("qwen-max") + 8 + 2 /* "qwen-max" + CRLF */);
|
||||
body_.set(body);
|
||||
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::Continue);
|
||||
last_body_size = body.size();
|
||||
|
||||
body = request_data.substr(0, request_data.find("qwen-max") + 8 + 2 + 50 /* "qwen-max" + CRLF */);
|
||||
body_.set(body);
|
||||
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::Continue);
|
||||
last_body_size = body.size();
|
||||
|
||||
body_.set(request_data);
|
||||
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, true), FilterDataStatus::Continue);
|
||||
}
|
||||
|
||||
} // namespace model_router
|
||||
|
||||
817
plugins/wasm-go/extensions/ai-proxy/provider/bedrock.go
Normal file
817
plugins/wasm-go/extensions/ai-proxy/provider/bedrock.go
Normal file
@@ -0,0 +1,817 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/log"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
)
|
||||
|
||||
const (
|
||||
httpPostMethod = "POST"
|
||||
awsService = "bedrock"
|
||||
// bedrock-runtime.{awsRegion}.amazonaws.com
|
||||
bedrockDefaultDomain = "bedrock-runtime.%s.amazonaws.com"
|
||||
// converse路径 /model/{modelId}/converse
|
||||
bedrockChatCompletionPath = "/model/%s/converse"
|
||||
// converseStream路径 /model/{modelId}/converse-stream
|
||||
bedrockStreamChatCompletionPath = "/model/%s/converse-stream"
|
||||
bedrockSignedHeaders = "host;x-amz-date"
|
||||
)
|
||||
|
||||
type bedrockProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (b *bedrockProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if len(config.awsAccessKey) == 0 || len(config.awsSecretKey) == 0 {
|
||||
return errors.New("missing bedrock access authentication parameters")
|
||||
}
|
||||
if len(config.awsRegion) == 0 {
|
||||
return errors.New("missing bedrock region parameters")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *bedrockProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): bedrockChatCompletionPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bedrockProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
config.setDefaultCapabilities(b.DefaultCapabilities())
|
||||
return &bedrockProvider{
|
||||
config: config,
|
||||
contextCache: createContextCache(&config),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type bedrockProvider struct {
|
||||
config ProviderConfig
|
||||
contextCache *contextCache
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) OnStreamingResponseBody(ctx wrapper.HttpContext, name ApiName, chunk []byte, isLastChunk bool) ([]byte, error) {
|
||||
events := extractAmazonEventStreamEvents(ctx, chunk)
|
||||
if len(events) == 0 {
|
||||
return chunk, fmt.Errorf("No events are extracted ")
|
||||
}
|
||||
var responseBuilder strings.Builder
|
||||
for _, event := range events {
|
||||
outputEvent, err := b.convertEventFromBedrockToOpenAI(ctx, event)
|
||||
if err != nil {
|
||||
log.Errorf("[onStreamingResponseBody] failed to process streaming event: %v\n%s", err, chunk)
|
||||
return chunk, err
|
||||
}
|
||||
responseBuilder.WriteString(string(outputEvent))
|
||||
}
|
||||
return []byte(responseBuilder.String()), nil
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) convertEventFromBedrockToOpenAI(ctx wrapper.HttpContext, bedrockEvent ConverseStreamEvent) ([]byte, error) {
|
||||
choices := make([]chatCompletionChoice, 0)
|
||||
chatChoice := chatCompletionChoice{
|
||||
Delta: &chatMessage{},
|
||||
}
|
||||
if bedrockEvent.Role != nil {
|
||||
chatChoice.Delta.Role = *bedrockEvent.Role
|
||||
}
|
||||
if bedrockEvent.Delta != nil {
|
||||
chatChoice.Delta = &chatMessage{Content: bedrockEvent.Delta.Text}
|
||||
}
|
||||
if bedrockEvent.StopReason != nil {
|
||||
chatChoice.FinishReason = stopReasonBedrock2OpenAI(*bedrockEvent.StopReason)
|
||||
}
|
||||
choices = append(choices, chatChoice)
|
||||
requestId := ctx.GetStringContext("X-Amzn-Requestid", "")
|
||||
openAIFormattedChunk := &chatCompletionResponse{
|
||||
Id: requestId,
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
Model: ctx.GetStringContext(ctxKeyFinalRequestModel, ""),
|
||||
SystemFingerprint: "",
|
||||
Object: objectChatCompletion,
|
||||
Choices: choices,
|
||||
}
|
||||
if bedrockEvent.Usage != nil {
|
||||
openAIFormattedChunk.Choices = choices[:0]
|
||||
openAIFormattedChunk.Usage = usage{
|
||||
CompletionTokens: bedrockEvent.Usage.OutputTokens,
|
||||
PromptTokens: bedrockEvent.Usage.InputTokens,
|
||||
TotalTokens: bedrockEvent.Usage.TotalTokens,
|
||||
}
|
||||
}
|
||||
|
||||
openAIFormattedChunkBytes, _ := json.Marshal(openAIFormattedChunk)
|
||||
var openAIChunk strings.Builder
|
||||
openAIChunk.WriteString(ssePrefix)
|
||||
openAIChunk.WriteString(string(openAIFormattedChunkBytes))
|
||||
openAIChunk.WriteString("\n\n")
|
||||
return []byte(openAIChunk.String()), nil
|
||||
}
|
||||
|
||||
type ConverseStreamEvent struct {
|
||||
ContentBlockIndex int `json:"contentBlockIndex,omitempty"`
|
||||
Delta *converseStreamEventContentBlockDelta `json:"delta,omitempty"`
|
||||
Role *string `json:"role,omitempty"`
|
||||
StopReason *string `json:"stopReason,omitempty"`
|
||||
Usage *tokenUsage `json:"usage,omitempty"`
|
||||
Start *contentBlockStart `json:"start,omitempty"`
|
||||
}
|
||||
|
||||
type converseStreamEventContentBlockDelta struct {
|
||||
Text *string `json:"text,omitempty"`
|
||||
ToolUse *toolUseBlockDelta `json:"toolUse,omitempty"`
|
||||
}
|
||||
|
||||
type toolUseBlockStart struct {
|
||||
Name string `json:"name"`
|
||||
ToolUseID string `json:"toolUseId"`
|
||||
}
|
||||
|
||||
type contentBlockStart struct {
|
||||
ToolUse *toolUseBlockStart `json:"toolUse,omitempty"`
|
||||
}
|
||||
|
||||
type toolUseBlockDelta struct {
|
||||
Input string `json:"input"`
|
||||
}
|
||||
|
||||
func extractAmazonEventStreamEvents(ctx wrapper.HttpContext, chunk []byte) []ConverseStreamEvent {
|
||||
body := chunk
|
||||
if bufferedStreamingBody, has := ctx.GetContext(ctxKeyStreamingBody).([]byte); has {
|
||||
body = append(bufferedStreamingBody, chunk...)
|
||||
}
|
||||
|
||||
r := bytes.NewReader(body)
|
||||
var events []ConverseStreamEvent
|
||||
var lastRead int64 = -1
|
||||
messageBuffer := make([]byte, 1024)
|
||||
defer func() {
|
||||
log.Infof("extractAmazonEventStreamEvents: lastRead=%d, r.Size=%d", lastRead, r.Size())
|
||||
ctx.SetContext(ctxKeyStreamingBody, nil)
|
||||
}()
|
||||
|
||||
for {
|
||||
msg, err := decodeMessage(r, messageBuffer)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
log.Errorf("failed to decode message: %v", err)
|
||||
break
|
||||
}
|
||||
var event ConverseStreamEvent
|
||||
if err = json.Unmarshal(msg.Payload, &event); err == nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
lastRead = r.Size() - int64(r.Len())
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
type bedrockStreamMessage struct {
|
||||
Headers headers
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
type EventFrame struct {
|
||||
TotalLength uint32
|
||||
HeadersLength uint32
|
||||
PreludeCRC uint32
|
||||
Headers map[string]interface{}
|
||||
Payload []byte
|
||||
PayloadCRC uint32
|
||||
}
|
||||
|
||||
type headers []header
|
||||
|
||||
type header struct {
|
||||
Name string
|
||||
Value Value
|
||||
}
|
||||
|
||||
func (hs *headers) Set(name string, value Value) {
|
||||
var i int
|
||||
for ; i < len(*hs); i++ {
|
||||
if (*hs)[i].Name == name {
|
||||
(*hs)[i].Value = value
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
*hs = append(*hs, header{
|
||||
Name: name, Value: value,
|
||||
})
|
||||
}
|
||||
|
||||
func decodeMessage(reader io.Reader, payloadBuf []byte) (m bedrockStreamMessage, err error) {
|
||||
crc := crc32.New(crc32.MakeTable(crc32.IEEE))
|
||||
hashReader := io.TeeReader(reader, crc)
|
||||
|
||||
prelude, err := decodePrelude(hashReader, crc)
|
||||
if err != nil {
|
||||
return bedrockStreamMessage{}, err
|
||||
}
|
||||
|
||||
if prelude.HeadersLen > 0 {
|
||||
lr := io.LimitReader(hashReader, int64(prelude.HeadersLen))
|
||||
m.Headers, err = decodeHeaders(lr)
|
||||
if err != nil {
|
||||
return bedrockStreamMessage{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if payloadLen := prelude.PayloadLen(); payloadLen > 0 {
|
||||
buf, err := decodePayload(payloadBuf, io.LimitReader(hashReader, int64(payloadLen)))
|
||||
if err != nil {
|
||||
return bedrockStreamMessage{}, err
|
||||
}
|
||||
m.Payload = buf
|
||||
}
|
||||
|
||||
msgCRC := crc.Sum32()
|
||||
if err := validateCRC(reader, msgCRC); err != nil {
|
||||
return bedrockStreamMessage{}, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func decodeHeaders(r io.Reader) (headers, error) {
|
||||
hs := headers{}
|
||||
|
||||
for {
|
||||
name, err := decodeHeaderName(r)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
// EOF while getting header name means no more headers
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
value, err := decodeHeaderValue(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hs.Set(name, value)
|
||||
}
|
||||
|
||||
return hs, nil
|
||||
}
|
||||
|
||||
func decodeHeaderValue(r io.Reader) (Value, error) {
|
||||
var raw rawValue
|
||||
|
||||
typ, err := decodeUint8(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw.Type = valueType(typ)
|
||||
|
||||
var v Value
|
||||
|
||||
switch raw.Type {
|
||||
case stringValueType:
|
||||
var tv StringValue
|
||||
err = tv.decode(r)
|
||||
v = tv
|
||||
default:
|
||||
log.Errorf("unknown value type %d", raw.Type)
|
||||
}
|
||||
|
||||
// Error could be EOF, let caller deal with it
|
||||
return v, err
|
||||
}
|
||||
|
||||
type Value interface {
|
||||
Get() interface{}
|
||||
}
|
||||
|
||||
type StringValue string
|
||||
|
||||
func (v StringValue) Get() interface{} {
|
||||
return string(v)
|
||||
}
|
||||
|
||||
func (v *StringValue) decode(r io.Reader) error {
|
||||
s, err := decodeStringValue(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*v = StringValue(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeBytesValue(r io.Reader) ([]byte, error) {
|
||||
var raw rawValue
|
||||
var err error
|
||||
raw.Len, err = decodeUint16(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf := make([]byte, raw.Len)
|
||||
_, err = io.ReadFull(r, buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func decodeUint16(r io.Reader) (uint16, error) {
|
||||
var b [2]byte
|
||||
bs := b[:]
|
||||
_, err := io.ReadFull(r, bs)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return binary.BigEndian.Uint16(bs), nil
|
||||
}
|
||||
|
||||
func decodeStringValue(r io.Reader) (string, error) {
|
||||
v, err := decodeBytesValue(r)
|
||||
return string(v), err
|
||||
}
|
||||
|
||||
type rawValue struct {
|
||||
Type valueType
|
||||
Len uint16 // Only set for variable length slices
|
||||
Value []byte // byte representation of value, BigEndian encoding.
|
||||
}
|
||||
|
||||
type valueType uint8
|
||||
|
||||
const (
|
||||
trueValueType valueType = iota
|
||||
falseValueType
|
||||
int8ValueType // Byte
|
||||
int16ValueType // Short
|
||||
int32ValueType // Integer
|
||||
int64ValueType // Long
|
||||
bytesValueType
|
||||
stringValueType
|
||||
timestampValueType
|
||||
uuidValueType
|
||||
)
|
||||
|
||||
func decodeHeaderName(r io.Reader) (string, error) {
|
||||
var n headerName
|
||||
|
||||
var err error
|
||||
n.Len, err = decodeUint8(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
name := n.Name[:n.Len]
|
||||
if _, err := io.ReadFull(r, name); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(name), nil
|
||||
}
|
||||
|
||||
func decodeUint8(r io.Reader) (uint8, error) {
|
||||
type byteReader interface {
|
||||
ReadByte() (byte, error)
|
||||
}
|
||||
|
||||
if br, ok := r.(byteReader); ok {
|
||||
v, err := br.ReadByte()
|
||||
return v, err
|
||||
}
|
||||
|
||||
var b [1]byte
|
||||
_, err := io.ReadFull(r, b[:])
|
||||
return b[0], err
|
||||
}
|
||||
|
||||
const maxHeaderNameLen = 255
|
||||
|
||||
type headerName struct {
|
||||
Len uint8
|
||||
Name [maxHeaderNameLen]byte
|
||||
}
|
||||
|
||||
func decodePayload(buf []byte, r io.Reader) ([]byte, error) {
|
||||
w := bytes.NewBuffer(buf[0:0])
|
||||
|
||||
_, err := io.Copy(w, r)
|
||||
return w.Bytes(), err
|
||||
}
|
||||
|
||||
type messagePrelude struct {
|
||||
Length uint32
|
||||
HeadersLen uint32
|
||||
PreludeCRC uint32
|
||||
}
|
||||
|
||||
func (p messagePrelude) ValidateLens() error {
|
||||
if p.Length == 0 {
|
||||
return fmt.Errorf("message prelude want: 16, have: %v", int(p.Length))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p messagePrelude) PayloadLen() uint32 {
|
||||
return p.Length - p.HeadersLen - 16
|
||||
}
|
||||
|
||||
func decodePrelude(r io.Reader, crc hash.Hash32) (messagePrelude, error) {
|
||||
var p messagePrelude
|
||||
|
||||
var err error
|
||||
p.Length, err = decodeUint32(r)
|
||||
if err != nil {
|
||||
return messagePrelude{}, err
|
||||
}
|
||||
|
||||
p.HeadersLen, err = decodeUint32(r)
|
||||
if err != nil {
|
||||
return messagePrelude{}, err
|
||||
}
|
||||
|
||||
if err := p.ValidateLens(); err != nil {
|
||||
return messagePrelude{}, err
|
||||
}
|
||||
|
||||
preludeCRC := crc.Sum32()
|
||||
if err := validateCRC(r, preludeCRC); err != nil {
|
||||
return messagePrelude{}, err
|
||||
}
|
||||
|
||||
p.PreludeCRC = preludeCRC
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func decodeUint32(r io.Reader) (uint32, error) {
|
||||
var b [4]byte
|
||||
bs := b[:]
|
||||
_, err := io.ReadFull(r, bs)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return binary.BigEndian.Uint32(bs), nil
|
||||
}
|
||||
|
||||
func validateCRC(r io.Reader, expect uint32) error {
|
||||
msgCRC, err := decodeUint32(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if msgCRC != expect {
|
||||
return fmt.Errorf("message checksum mismatch")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) TransformResponseHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header) {
|
||||
ctx.SetContext("X-Amzn-Requestid", headers.Get("X-Amzn-Requestid"))
|
||||
if headers.Get("Content-Type") == "application/vnd.amazon.eventstream" {
|
||||
headers.Set("Content-Type", "text/event-stream; charset=utf-8")
|
||||
}
|
||||
headers.Del("Content-Length")
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) GetProviderType() string {
|
||||
return providerTypeBedrock
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName) error {
|
||||
b.config.handleRequestHeaders(b, ctx, apiName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) TransformRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header) {
|
||||
util.OverwriteRequestHostHeader(headers, fmt.Sprintf(bedrockDefaultDomain, b.config.awsRegion))
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte) (types.Action, error) {
|
||||
if !b.config.isSupportedAPI(apiName) {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
return b.config.handleRequestBody(b, b.contextCache, ctx, apiName, body)
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) insertHttpContextMessage(body []byte, content string, onlyOneSystemBeforeFile bool) ([]byte, error) {
|
||||
request := &bedrockTextGenRequest{}
|
||||
if err := json.Unmarshal(body, request); err != nil {
|
||||
return nil, fmt.Errorf("unable to unmarshal request: %v", err)
|
||||
}
|
||||
|
||||
if len(request.System) > 0 {
|
||||
request.System = append(request.System, systemContentBlock{Text: content})
|
||||
} else {
|
||||
request.System = []systemContentBlock{{Text: content}}
|
||||
}
|
||||
|
||||
requestBytes, err := json.Marshal(request)
|
||||
b.setAuthHeaders(requestBytes, nil)
|
||||
return requestBytes, err
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) TransformRequestBodyHeaders(ctx wrapper.HttpContext, apiName ApiName, body []byte, headers http.Header) ([]byte, error) {
|
||||
switch apiName {
|
||||
case ApiNameChatCompletion:
|
||||
return b.onChatCompletionRequestBody(ctx, body, headers)
|
||||
default:
|
||||
return b.config.defaultTransformRequestBody(ctx, apiName, body)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) TransformResponseBody(ctx wrapper.HttpContext, apiName ApiName, body []byte) ([]byte, error) {
|
||||
if apiName == ApiNameChatCompletion {
|
||||
return b.onChatCompletionResponseBody(ctx, body)
|
||||
}
|
||||
return nil, errUnsupportedApiName
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) onChatCompletionResponseBody(ctx wrapper.HttpContext, body []byte) ([]byte, error) {
|
||||
bedrockResponse := &bedrockConverseResponse{}
|
||||
if err := json.Unmarshal(body, bedrockResponse); err != nil {
|
||||
log.Errorf("unable to unmarshal bedrock response: %v", err)
|
||||
return nil, fmt.Errorf("unable to unmarshal bedrock response: %v", err)
|
||||
}
|
||||
response := b.buildChatCompletionResponse(ctx, bedrockResponse)
|
||||
return json.Marshal(response)
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) onChatCompletionRequestBody(ctx wrapper.HttpContext, body []byte, headers http.Header) ([]byte, error) {
|
||||
request := &chatCompletionRequest{}
|
||||
err := b.config.parseRequestAndMapModel(ctx, request, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
streaming := request.Stream
|
||||
headers.Set("Accept", "*/*")
|
||||
if streaming {
|
||||
util.OverwriteRequestPathHeader(headers, fmt.Sprintf(bedrockStreamChatCompletionPath, request.Model))
|
||||
} else {
|
||||
util.OverwriteRequestPathHeader(headers, fmt.Sprintf(bedrockChatCompletionPath, request.Model))
|
||||
}
|
||||
return b.buildBedrockTextGenerationRequest(request, headers)
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) buildBedrockTextGenerationRequest(origRequest *chatCompletionRequest, headers http.Header) ([]byte, error) {
|
||||
messages := make([]bedrockMessage, 0, len(origRequest.Messages))
|
||||
for i := range origRequest.Messages {
|
||||
messages = append(messages, chatMessage2BedrockMessage(origRequest.Messages[i]))
|
||||
}
|
||||
request := &bedrockTextGenRequest{
|
||||
Messages: messages,
|
||||
InferenceConfig: bedrockInferenceConfig{
|
||||
MaxTokens: origRequest.MaxTokens,
|
||||
Temperature: origRequest.Temperature,
|
||||
TopP: origRequest.TopP,
|
||||
},
|
||||
AdditionalModelRequestFields: map[string]interface{}{},
|
||||
PerformanceConfig: PerformanceConfiguration{
|
||||
Latency: "standard",
|
||||
},
|
||||
}
|
||||
requestBytes, err := json.Marshal(request)
|
||||
b.setAuthHeaders(requestBytes, headers)
|
||||
return requestBytes, err
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) buildChatCompletionResponse(ctx wrapper.HttpContext, bedrockResponse *bedrockConverseResponse) *chatCompletionResponse {
|
||||
var outputContent string
|
||||
if len(bedrockResponse.Output.Message.Content) > 0 {
|
||||
outputContent = bedrockResponse.Output.Message.Content[0].Text
|
||||
}
|
||||
choices := []chatCompletionChoice{
|
||||
{
|
||||
Index: 0,
|
||||
Message: &chatMessage{
|
||||
Role: bedrockResponse.Output.Message.Role,
|
||||
Content: outputContent,
|
||||
},
|
||||
FinishReason: stopReasonBedrock2OpenAI(bedrockResponse.StopReason),
|
||||
},
|
||||
}
|
||||
requestId := ctx.GetStringContext("X-Amzn-Requestid", "")
|
||||
return &chatCompletionResponse{
|
||||
Id: requestId,
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
Model: ctx.GetStringContext(ctxKeyFinalRequestModel, ""),
|
||||
SystemFingerprint: "",
|
||||
Object: objectChatCompletion,
|
||||
Choices: choices,
|
||||
Usage: usage{
|
||||
PromptTokens: bedrockResponse.Usage.InputTokens,
|
||||
CompletionTokens: bedrockResponse.Usage.OutputTokens,
|
||||
TotalTokens: bedrockResponse.Usage.TotalTokens,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func stopReasonBedrock2OpenAI(reason string) string {
|
||||
switch reason {
|
||||
case "end_turn":
|
||||
return finishReasonStop
|
||||
case "stop_sequence":
|
||||
return finishReasonStop
|
||||
case "max_tokens":
|
||||
return finishReasonLength
|
||||
default:
|
||||
return reason
|
||||
}
|
||||
}
|
||||
|
||||
type bedrockTextGenRequest struct {
|
||||
Messages []bedrockMessage `json:"messages"`
|
||||
System []systemContentBlock `json:"system,omitempty"`
|
||||
InferenceConfig bedrockInferenceConfig `json:"inferenceConfig,omitempty"`
|
||||
AdditionalModelRequestFields map[string]interface{} `json:"additionalModelRequestFields,omitempty"`
|
||||
PerformanceConfig PerformanceConfiguration `json:"performanceConfig,omitempty"`
|
||||
}
|
||||
|
||||
type PerformanceConfiguration struct {
|
||||
Latency string `json:"latency,omitempty"`
|
||||
}
|
||||
|
||||
type bedrockMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content []bedrockMessageContent `json:"content"`
|
||||
}
|
||||
|
||||
type bedrockMessageContent struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
Image *imageBlock `json:"image,omitempty"`
|
||||
}
|
||||
|
||||
type systemContentBlock struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
type imageBlock struct {
|
||||
Format string `json:"format,omitempty"`
|
||||
Source imageSource `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
type imageSource struct {
|
||||
Bytes string `json:"bytes,omitempty"`
|
||||
}
|
||||
|
||||
type bedrockInferenceConfig struct {
|
||||
StopSequences []string `json:"stopSequences,omitempty"`
|
||||
MaxTokens int `json:"maxTokens,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
TopP float64 `json:"topP,omitempty"`
|
||||
}
|
||||
|
||||
type bedrockConverseResponse struct {
|
||||
Metrics converseMetrics `json:"metrics"`
|
||||
Output converseOutputMemberMessage `json:"output"`
|
||||
StopReason string `json:"stopReason"`
|
||||
Usage tokenUsage `json:"usage"`
|
||||
}
|
||||
|
||||
type converseMetrics struct {
|
||||
LatencyMs int `json:"latencyMs"`
|
||||
}
|
||||
|
||||
type converseOutputMemberMessage struct {
|
||||
Message message `json:"message"`
|
||||
}
|
||||
|
||||
type message struct {
|
||||
Content []contentBlockMemberText `json:"content"`
|
||||
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
type contentBlockMemberText struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type tokenUsage struct {
|
||||
InputTokens int `json:"inputTokens,omitempty"`
|
||||
|
||||
OutputTokens int `json:"outputTokens,omitempty"`
|
||||
|
||||
TotalTokens int `json:"totalTokens"`
|
||||
}
|
||||
|
||||
func chatMessage2BedrockMessage(chatMessage chatMessage) bedrockMessage {
|
||||
if chatMessage.IsStringContent() {
|
||||
return bedrockMessage{
|
||||
Role: chatMessage.Role,
|
||||
Content: []bedrockMessageContent{{Text: chatMessage.StringContent()}},
|
||||
}
|
||||
} else {
|
||||
var contents []bedrockMessageContent
|
||||
openaiContent := chatMessage.ParseContent()
|
||||
for _, part := range openaiContent {
|
||||
var content bedrockMessageContent
|
||||
if part.Type == contentTypeText {
|
||||
content.Text = part.Text
|
||||
} else {
|
||||
log.Warnf("imageUrl is not supported: %s", part.Type)
|
||||
continue
|
||||
}
|
||||
contents = append(contents, content)
|
||||
}
|
||||
return bedrockMessage{
|
||||
Role: chatMessage.Role,
|
||||
Content: contents,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) setAuthHeaders(body []byte, headers http.Header) {
|
||||
t := time.Now().UTC()
|
||||
amzDate := t.Format("20060102T150405Z")
|
||||
dateStamp := t.Format("20060102")
|
||||
path, _ := proxywasm.GetHttpRequestHeader(":path")
|
||||
if headers != nil {
|
||||
path = headers.Get(":path")
|
||||
}
|
||||
signature := b.generateSignature(path, amzDate, dateStamp, body)
|
||||
if headers != nil {
|
||||
headers.Set("X-Amz-Date", amzDate)
|
||||
headers.Set("Authorization", fmt.Sprintf("AWS4-HMAC-SHA256 Credential=%s/%s/%s/%s/aws4_request, SignedHeaders=%s, Signature=%s", b.config.awsAccessKey, dateStamp, b.config.awsRegion, awsService, bedrockSignedHeaders, signature))
|
||||
} else {
|
||||
_ = proxywasm.ReplaceHttpRequestHeader("X-Amz-Date", amzDate)
|
||||
_ = proxywasm.ReplaceHttpRequestHeader("Authorization", fmt.Sprintf("AWS4-HMAC-SHA256 Credential=%s/%s/%s/%s/aws4_request, SignedHeaders=%s, Signature=%s", b.config.awsAccessKey, dateStamp, b.config.awsRegion, awsService, bedrockSignedHeaders, signature))
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) generateSignature(path, amzDate, dateStamp string, body []byte) string {
|
||||
hashedPayload := sha256Hex(body)
|
||||
path = urlEncoding(path)
|
||||
|
||||
endpoint := fmt.Sprintf(bedrockDefaultDomain, b.config.awsRegion)
|
||||
canonicalHeaders := fmt.Sprintf("host:%s\nx-amz-date:%s\n", endpoint, amzDate)
|
||||
canonicalRequest := fmt.Sprintf("%s\n%s\n\n%s\n%s\n%s",
|
||||
httpPostMethod, path, canonicalHeaders, bedrockSignedHeaders, hashedPayload)
|
||||
|
||||
credentialScope := fmt.Sprintf("%s/%s/%s/aws4_request", dateStamp, b.config.awsRegion, awsService)
|
||||
hashedCanonReq := sha256Hex([]byte(canonicalRequest))
|
||||
stringToSign := fmt.Sprintf("AWS4-HMAC-SHA256\n%s\n%s\n%s",
|
||||
amzDate, credentialScope, hashedCanonReq)
|
||||
|
||||
signingKey := getSignatureKey(b.config.awsSecretKey, dateStamp, b.config.awsRegion, awsService)
|
||||
signature := hmacHex(signingKey, stringToSign)
|
||||
return signature
|
||||
}
|
||||
|
||||
func urlEncoding(rawStr string) string {
|
||||
encodedStr := strings.ReplaceAll(rawStr, ":", "%3A")
|
||||
encodedStr = strings.ReplaceAll(encodedStr, "+", "%2B")
|
||||
encodedStr = strings.ReplaceAll(encodedStr, "=", "%3D")
|
||||
encodedStr = strings.ReplaceAll(encodedStr, "&", "%26")
|
||||
encodedStr = strings.ReplaceAll(encodedStr, "$", "%24")
|
||||
encodedStr = strings.ReplaceAll(encodedStr, "@", "%40")
|
||||
return encodedStr
|
||||
}
|
||||
|
||||
func getSignatureKey(key, dateStamp, region, service string) []byte {
|
||||
kDate := hmacSha256([]byte("AWS4"+key), dateStamp)
|
||||
kRegion := hmacSha256(kDate, region)
|
||||
kService := hmacSha256(kRegion, service)
|
||||
kSigning := hmacSha256(kService, "aws4_request")
|
||||
return kSigning
|
||||
}
|
||||
|
||||
func hmacSha256(key []byte, data string) []byte {
|
||||
h := hmac.New(sha256.New, key)
|
||||
h.Write([]byte(data))
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func sha256Hex(data []byte) string {
|
||||
h := sha256.New()
|
||||
h.Write(data)
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func hmacHex(key []byte, data string) string {
|
||||
h := hmac.New(sha256.New, key)
|
||||
h.Write([]byte(data))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
@@ -68,6 +68,7 @@ const (
|
||||
providerTypeCoze = "coze"
|
||||
providerTypeTogetherAI = "together-ai"
|
||||
providerTypeDify = "dify"
|
||||
providerTypeBedrock = "bedrock"
|
||||
|
||||
protocolOpenAI = "openai"
|
||||
protocolOriginal = "original"
|
||||
@@ -138,6 +139,7 @@ var (
|
||||
providerTypeCoze: &cozeProviderInitializer{},
|
||||
providerTypeTogetherAI: &togetherAIProviderInitializer{},
|
||||
providerTypeDify: &difyProviderInitializer{},
|
||||
providerTypeBedrock: &bedrockProviderInitializer{},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -242,6 +244,15 @@ type ProviderConfig struct {
|
||||
// @Title zh-CN hunyuan api id for authorization
|
||||
// @Description zh-CN 仅适用于Hun Yuan AI服务鉴权
|
||||
hunyuanAuthId string `required:"false" yaml:"hunyuanAuthId" json:"hunyuanAuthId"`
|
||||
// @Title zh-CN Amazon Bedrock AccessKey for authorization
|
||||
// @Description zh-CN 仅适用于Amazon Bedrock服务鉴权,API key/id 参考:https://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/reference_sigv.html
|
||||
awsAccessKey string `required:"false" yaml:"awsAccessKey" json:"awsAccessKey"`
|
||||
// @Title zh-CN Amazon Bedrock SecretKey for authorization
|
||||
// @Description zh-CN 仅适用于Amazon Bedrock服务鉴权
|
||||
awsSecretKey string `required:"false" yaml:"awsSecretKey" json:"awsSecretKey"`
|
||||
// @Title zh-CN Amazon Bedrock Region
|
||||
// @Description zh-CN 仅适用于Amazon Bedrock服务访问
|
||||
awsRegion string `required:"false" yaml:"awsRegion" json:"awsRegion"`
|
||||
// @Title zh-CN minimax API type
|
||||
// @Description zh-CN 仅适用于 minimax 服务。minimax API 类型,v2 和 pro 中选填一项,默认值为 v2
|
||||
minimaxApiType string `required:"false" yaml:"minimaxApiType" json:"minimaxApiType"`
|
||||
@@ -346,6 +357,9 @@ func (c *ProviderConfig) FromJson(json gjson.Result) {
|
||||
c.claudeVersion = json.Get("claudeVersion").String()
|
||||
c.hunyuanAuthId = json.Get("hunyuanAuthId").String()
|
||||
c.hunyuanAuthKey = json.Get("hunyuanAuthKey").String()
|
||||
c.awsAccessKey = json.Get("awsAccessKey").String()
|
||||
c.awsSecretKey = json.Get("awsSecretKey").String()
|
||||
c.awsRegion = json.Get("awsRegion").String()
|
||||
c.minimaxApiType = json.Get("minimaxApiType").String()
|
||||
c.minimaxGroupId = json.Get("minimaxGroupId").String()
|
||||
c.cloudflareAccountId = json.Get("cloudflareAccountId").String()
|
||||
|
||||
@@ -75,18 +75,22 @@ description: higress 支持通过集成搜索引擎(Google/Bing/Arxiv/Elastics
|
||||
|
||||
## Elasticsearch 特定配置
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|------|----------|----------|--------|-----------------------|
|
||||
| index | string | 必填 | - | 要搜索的Elasticsearch索引名称 |
|
||||
| contentField | string | 必填 | - | 要查询的内容字段名称 |
|
||||
| semanticTextField | string | 必填 | - | 要查询的 embedding 字段名称 |
|
||||
| linkField | string | 必填 | - | 结果链接字段名称 |
|
||||
| titleField | string | 必填 | - | 结果标题字段名称 |
|
||||
| username | string | 选填 | - | Elasticsearch 用户名 |
|
||||
| password | string | 选填 | - | Elasticsearch 密码 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|------|----------|------|--------|------------------------------------|
|
||||
| index | string | 必填 | - | 要搜索的 Elasticsearch 索引名称 |
|
||||
| contentField | string | 必填 | - | 要查询的内容字段名称 |
|
||||
| semanticTextField | string | 必填 | - | 要查询的 embedding 字段名称 |
|
||||
| linkField | string | 选填 | - | 结果链接字段名称,当配置 `needReference` 时需要填写 |
|
||||
| titleField | string | 选填 | - | 结果标题字段名称,当配置 `needReference` 时需要填写 |
|
||||
| username | string | 选填 | - | Elasticsearch 用户名 |
|
||||
| password | string | 选填 | - | Elasticsearch 密码 |
|
||||
|
||||
混合搜索中使用的 [Reciprocal Rank Fusion (RRF)](https://www.elastic.co/guide/en/elasticsearch/reference/8.17/rrf.html) 查询要求 Elasticsearch 的版本在 8.8 及以上。
|
||||
|
||||
目前文档向量化依赖于 Elasticsearch 的 Embedding 模型,该功能需要 Elasticsearch 企业版 License,或可使用 30 天的 Trial License。安装 Elasticsearch 内置 Embedding 模型的步骤可参考[该文档](https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-elser#alternative-download-deploy);若需安装第三方 Embedding 模型,可参考[该文档](https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-text-emb-vector-search-example)。
|
||||
|
||||
有关 ai-search 插件集成 Elasticsearch 的完整教程,请参考:[使用 LangChain + Higress + Elasticsearch 构建 RAG 应用](https://cr7258.github.io/blogs/original/2025/15-rag-higress-es-langchain)。
|
||||
|
||||
## Quark 特定配置
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
@@ -204,13 +208,9 @@ searchFrom:
|
||||
searchFrom:
|
||||
- type: elasticsearch
|
||||
serviceName: "es-svc.static"
|
||||
# 固定地址服务的端口默认是80
|
||||
servicePort: 80
|
||||
index: "knowledge_base"
|
||||
contentField: "content"
|
||||
semanticTextField: "semantic_text"
|
||||
linkField: "url"
|
||||
titleField: "title"
|
||||
# username: "elastic"
|
||||
# password: "password"
|
||||
```
|
||||
|
||||
@@ -80,13 +80,17 @@ It is strongly recommended to enable this feature when using Arxiv or Elasticsea
|
||||
| index | string | Required | - | Elasticsearch index name to search |
|
||||
| contentField | string | Required | - | Content field name to query |
|
||||
| semanticTextField | string | Required | - | Embedding field name to query |
|
||||
| linkField | string | Required | - | Result link field name |
|
||||
| titleField | string | Required | - | Result title field name |
|
||||
| linkField | string | Optional | - | Result link field name, needed when `needReference` is configured |
|
||||
| titleField | string | Optional | - | Result title field name, needed when `needReference` is configured |
|
||||
| username | string | Optional | - | Elasticsearch username |
|
||||
| password | string | Optional | - | Elasticsearch password |
|
||||
|
||||
The [Reciprocal Rank Fusion (RRF)](https://www.elastic.co/guide/en/elasticsearch/reference/8.17/rrf.html) query used in hybrid search requires Elasticsearch version 8.8 or higher.
|
||||
|
||||
Currently, document vectorization relies on Elasticsearch's embedding model, which requires an Elasticsearch Enterprise license or a 30-day Trial license. To install the built-in embedding model in Elasticsearch, please refer to [this documentation](https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-elser#alternative-download-deploy). If you want to install a third-party embedding model, please refer to [this guide](https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-text-emb-vector-search-example).
|
||||
|
||||
For a complete tutorial on integrating the ai-search plugin with Elasticsearch, please refer to: [Building a RAG Application with LangChain + Higress + Elasticsearch](https://cr7258.github.io/blogs/original/2025/15-rag-higress-es-langchain).
|
||||
|
||||
## Quark Specific Configuration
|
||||
|
||||
| Name | Data Type | Requirement | Default Value | Description |
|
||||
@@ -203,13 +207,9 @@ Note that excessive concurrency may lead to rate limiting, adjust according to a
|
||||
searchFrom:
|
||||
- type: elasticsearch
|
||||
serviceName: "es-svc.static"
|
||||
# static ip service use 80 as default port
|
||||
servicePort: 80
|
||||
index: "knowledge_base"
|
||||
contentField: "content"
|
||||
semanticTextField: "semantic_text"
|
||||
linkField: "url"
|
||||
titleField: "title"
|
||||
# username: "elastic"
|
||||
# password: "password"
|
||||
```
|
||||
|
||||
@@ -27,7 +27,7 @@ type ElasticsearchSearch struct {
|
||||
password string
|
||||
}
|
||||
|
||||
func NewElasticsearchSearch(config *gjson.Result) (*ElasticsearchSearch, error) {
|
||||
func NewElasticsearchSearch(config *gjson.Result, needReference bool) (*ElasticsearchSearch, error) {
|
||||
engine := &ElasticsearchSearch{}
|
||||
serviceName := config.Get("serviceName").String()
|
||||
if serviceName == "" {
|
||||
@@ -35,7 +35,13 @@ func NewElasticsearchSearch(config *gjson.Result) (*ElasticsearchSearch, error)
|
||||
}
|
||||
servicePort := config.Get("servicePort").Int()
|
||||
if servicePort == 0 {
|
||||
return nil, errors.New("servicePort not found")
|
||||
if strings.HasSuffix(serviceName, ".static") {
|
||||
servicePort = 80
|
||||
} else if strings.HasSuffix(serviceName, ".dns") {
|
||||
servicePort = 443
|
||||
} else {
|
||||
return nil, errors.New("servicePort not found")
|
||||
}
|
||||
}
|
||||
engine.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: serviceName,
|
||||
@@ -54,14 +60,18 @@ func NewElasticsearchSearch(config *gjson.Result) (*ElasticsearchSearch, error)
|
||||
if engine.semanticTextField == "" {
|
||||
return nil, errors.New("semanticTextField not found")
|
||||
}
|
||||
engine.linkField = config.Get("linkField").String()
|
||||
if engine.linkField == "" {
|
||||
return nil, errors.New("linkField not found")
|
||||
}
|
||||
engine.titleField = config.Get("titleField").String()
|
||||
if engine.titleField == "" {
|
||||
return nil, errors.New("titleField not found")
|
||||
|
||||
if needReference {
|
||||
engine.linkField = config.Get("linkField").String()
|
||||
if engine.linkField == "" {
|
||||
return nil, errors.New("linkField not found")
|
||||
}
|
||||
engine.titleField = config.Get("titleField").String()
|
||||
if engine.titleField == "" {
|
||||
return nil, errors.New("titleField not found")
|
||||
}
|
||||
}
|
||||
|
||||
engine.timeoutMillisecond = uint32(config.Get("timeoutMillisecond").Uint())
|
||||
if engine.timeoutMillisecond == 0 {
|
||||
engine.timeoutMillisecond = 5000
|
||||
@@ -93,6 +103,9 @@ func (e ElasticsearchSearch) generateAuthorizationHeader() string {
|
||||
func (e ElasticsearchSearch) generateQueryBody(ctx engine.SearchContext) string {
|
||||
queryText := strings.Join(ctx.Querys, " ")
|
||||
return fmt.Sprintf(`{
|
||||
"_source":{
|
||||
"excludes": "%s"
|
||||
},
|
||||
"retriever": {
|
||||
"rrf": {
|
||||
"retrievers": [
|
||||
@@ -118,7 +131,7 @@ func (e ElasticsearchSearch) generateQueryBody(ctx engine.SearchContext) string
|
||||
]
|
||||
}
|
||||
}
|
||||
}`, e.contentField, queryText, e.semanticTextField, queryText)
|
||||
}`, e.semanticTextField, e.contentField, queryText, e.semanticTextField, queryText)
|
||||
}
|
||||
|
||||
func (e ElasticsearchSearch) CallArgs(ctx engine.SearchContext) engine.CallArgs {
|
||||
@@ -145,9 +158,7 @@ func (e ElasticsearchSearch) ParseResult(ctx engine.SearchContext, response []by
|
||||
Link: source.Get(e.linkField).String(),
|
||||
Content: source.Get(e.contentField).String(),
|
||||
}
|
||||
if result.Valid() {
|
||||
results = append(results, result)
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ func parseConfig(json gjson.Result, config *Config, log wrapper.Log) error {
|
||||
arxivExists = true
|
||||
onlyQuark = false
|
||||
case "elasticsearch":
|
||||
searchEngine, err := elasticsearch.NewElasticsearchSearch(&e)
|
||||
searchEngine, err := elasticsearch.NewElasticsearchSearch(&e, config.needReference)
|
||||
if err != nil {
|
||||
return fmt.Errorf("elasticsearch search engine init failed:%s", err)
|
||||
}
|
||||
|
||||
@@ -6,11 +6,9 @@ replace quark-search => ../quark-search
|
||||
|
||||
replace amap-tools => ../amap-tools
|
||||
|
||||
replace github.com/alibaba/higress/plugins/wasm-go => /Users/zhangty/tmp/higress/plugins/wasm-go
|
||||
|
||||
require (
|
||||
amap-tools v0.0.0-00010101000000-000000000000
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250417132640-fb2e8d5157ad
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250423015849-23258157a406
|
||||
quark-search v0.0.0-00010101000000-000000000000
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+
|
||||
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250423015849-23258157a406 h1:pWZsjfarQyUPlzJ9CMy4C5iHl0jb2jntscd1wCGwGB0=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250423015849-23258157a406/go.mod h1:yObZXF1xTx/8peEsSbtHIzz7KlTr/tZCrokIHtwF0Jk=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
|
||||
@@ -28,7 +28,7 @@ On the user's MCP Client interface, add the generated SSE URL to the MCP Server
|
||||
```json
|
||||
"mcpServers": {
|
||||
"bravesearch": {
|
||||
"url": "http://mcp.higress.ai/mcp-brave-search/{generate_key}",
|
||||
"url": "https://mcp.higress.ai/mcp-bravesearch/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
```json
|
||||
"mcpServers": {
|
||||
"bravesearch": {
|
||||
"url": "http://mcp.higress.ai/mcp-brave-search/{generate_key}",
|
||||
"url": "https://mcp.higress.ai/mcp-bravesearch/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -4,9 +4,9 @@ server:
|
||||
apiKey: ""
|
||||
tools:
|
||||
- name: brave_web_search
|
||||
description: "使用Brave Search API进行网页搜索,适用于一般查询、新闻、文章和在线内容。支持分页、内容过滤和新鲜度控制。每次请求最多返回20条结果。"
|
||||
description: "使用Brave Search API进行网页搜索,适用于一般查询、新闻、文章和在线内容。支持分页、内容过滤和新鲜度控制。"
|
||||
args:
|
||||
- name: query
|
||||
- name: q
|
||||
description: "搜索查询(最多400字符,50个词)"
|
||||
type: string
|
||||
required: true
|
||||
@@ -20,6 +20,11 @@ tools:
|
||||
type: integer
|
||||
required: false
|
||||
default: 0
|
||||
- name: search_lang
|
||||
description: "搜索语言"
|
||||
type: string
|
||||
required: false
|
||||
enum: ["en", "zh-hans"]
|
||||
requestTemplate:
|
||||
url: "https://api.search.brave.com/res/v1/web/search"
|
||||
method: GET
|
||||
@@ -27,8 +32,6 @@ tools:
|
||||
headers:
|
||||
- key: Accept
|
||||
value: "application/json"
|
||||
- key: Accept-Encoding
|
||||
value: "gzip"
|
||||
- key: X-Subscription-Token
|
||||
value: "{{.config.apiKey}}"
|
||||
responseTemplate:
|
||||
@@ -39,38 +42,80 @@ tools:
|
||||
- **描述**: {{ $item.description }}
|
||||
- **URL**: {{ $item.url }}
|
||||
{{- end }}
|
||||
{{- if .locations.results }}
|
||||
{{- range $index, $item := .locations.results }}
|
||||
## 结果 {{add $index 1}}
|
||||
- **locationID**: {{ $item.id }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
- name: brave_local_search
|
||||
description: "使用Brave的Local Search API搜索本地商家和地点。适用于与物理位置、商家、餐厅、服务等相关的查询。返回详细信息包括商家名称、地址、评分、评论数、电话号码、营业时间等。如果没有本地结果,会自动回退到网页搜索。"
|
||||
- name: brave_local_search_pois
|
||||
description: "使用Brave Local Search API搜索本地POI(兴趣点)信息,包括名称、地址、电话、评分等信息。"
|
||||
args:
|
||||
- name: query
|
||||
description: "本地搜索查询(例如'Central Park附近的披萨')"
|
||||
type: string
|
||||
- name: ids
|
||||
description: "Location ID列表,通过brave_web_search获取"
|
||||
type: array
|
||||
required: true
|
||||
- name: count
|
||||
description: "结果数量(1-20,默认5)"
|
||||
type: integer
|
||||
- name: search_lang
|
||||
description: "搜索语言"
|
||||
type: string
|
||||
required: false
|
||||
default: 5
|
||||
default: "en"
|
||||
- name: search_lang
|
||||
description: "响应语言"
|
||||
type: string
|
||||
required: false
|
||||
default: "en-US"
|
||||
requestTemplate:
|
||||
url: "https://api.search.brave.com/res/v1/web/search"
|
||||
url: "https://api.search.brave.com/res/v1/local/pois"
|
||||
method: GET
|
||||
argsToUrlParam: true
|
||||
headers:
|
||||
- key: Accept
|
||||
value: "application/json"
|
||||
- key: Accept-Encoding
|
||||
value: "gzip"
|
||||
- key: X-Subscription-Token
|
||||
value: "{{.config.apiKey}}"
|
||||
responseTemplate:
|
||||
body: |
|
||||
{{- range $index, $item := .results }}
|
||||
## 结果 {{add $index 1}}
|
||||
## POI {{add $index 1}}
|
||||
- **名称**: {{ $item.name }}
|
||||
- **地址**: {{ $item.address.streetAddress }}, {{ $item.address.addressLocality }}, {{ $item.address.addressRegion }} {{ $item.address.postalCode }}
|
||||
- **电话**: {{ $item.phone }}
|
||||
- **评分**: {{ $item.rating.ratingValue }} ({{ $item.rating.ratingCount }} 条评论)
|
||||
- **评分**: {{ $item.rating.ratingValue }} ({{ $item.rating.ratingCount }} 条评价)
|
||||
- **价格范围**: {{ $item.priceRange }}
|
||||
- **营业时间**: {{ join $item.openingHours ", " }}
|
||||
{{- end }}
|
||||
|
||||
- name: brave_local_search_descriptions
|
||||
description: "使用Brave Local Search API获取本地POI的描述信息。"
|
||||
args:
|
||||
- name: ids
|
||||
description: "Location ID列表,通过brave_web_search获取"
|
||||
type: array
|
||||
required: true
|
||||
- name: search_lang
|
||||
description: "搜索语言"
|
||||
type: string
|
||||
required: false
|
||||
default: "en"
|
||||
- name: search_lang
|
||||
description: "响应语言"
|
||||
type: string
|
||||
required: false
|
||||
default: "en-US"
|
||||
requestTemplate:
|
||||
url: "https://api.search.brave.com/res/v1/local/descriptions"
|
||||
method: GET
|
||||
argsToUrlParam: true
|
||||
headers:
|
||||
- key: Accept
|
||||
value: "application/json"
|
||||
- key: X-Subscription-Token
|
||||
value: "{{.config.apiKey}}"
|
||||
responseTemplate:
|
||||
body: |
|
||||
{{- range $id, $desc := .descriptions }}
|
||||
## 描述 {{ $id }}
|
||||
{{ $desc }}
|
||||
{{- end }}
|
||||
|
||||
@@ -21,7 +21,7 @@ On the user's MCP Client interface, add the generated SSE URL to the MCP Server
|
||||
```json
|
||||
"mcpServers": {
|
||||
"chatppt": {
|
||||
"url": "http://mcp.higress.ai/mcp-chatppt/{generate_key}",
|
||||
"url": "https://mcp.higress.ai/mcp-chatppt/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
```json
|
||||
"mcpServers": {
|
||||
"chatppt": {
|
||||
"url": "http://mcp.higress.ai/mcp-chatppt/{generate_key}",
|
||||
"url": "https://mcp.higress.ai/mcp-chatppt/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
49
plugins/wasm-go/mcp-servers/mcp-context7/README.md
Normal file
49
plugins/wasm-go/mcp-servers/mcp-context7/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Context7 MCP Server
|
||||
|
||||
An implementation of the Model Context Protocol (MCP) server that integrates [Context7](https://context7.com), providing up-to-date, version-specific documentation and code examples.
|
||||
|
||||
Source Code: [https://github.com/upstash/context7](https://github.com/upstash/context7)
|
||||
|
||||
## Features
|
||||
|
||||
- Get up-to-date, version-specific documentation
|
||||
- Extract real, working code examples from source
|
||||
- Provide concise, relevant information without filler
|
||||
- Free for personal use
|
||||
- Integration with your MCP server and tools
|
||||
|
||||
## Usage Guide
|
||||
|
||||
### Generate SSE URL
|
||||
|
||||
On the MCP Server interface, log in and enter the API-KEY to generate the URL.
|
||||
|
||||
### Configure MCP Client
|
||||
|
||||
On the user's MCP Client interface, add the generated SSE URL to the MCP Server list.
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"context7": {
|
||||
"url": "https://mcp.higress.ai/mcp-context7/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Available Tools
|
||||
|
||||
#### resolve-library-id
|
||||
Resolves a general package name into a Context7-compatible library ID. This is a required first step before using the get-library-docs tool.
|
||||
|
||||
Parameters:
|
||||
- query: Library name to search for and retrieve a Context7-compatible library ID (required)
|
||||
|
||||
#### get-library-docs
|
||||
Fetches up-to-date documentation for a library. You must call resolve-library-id first to obtain the exact Context7-compatible library ID.
|
||||
|
||||
Parameters:
|
||||
- folders: Folders filter for organizing documentation
|
||||
- libraryId: Unique identifier of the library (required)
|
||||
- tokens: Maximum number of tokens to return (default: 5000)
|
||||
- topic: Specific topic within the documentation
|
||||
- type: Type of documentation to retrieve (currently only "txt" supported)
|
||||
49
plugins/wasm-go/mcp-servers/mcp-context7/README_ZH.md
Normal file
49
plugins/wasm-go/mcp-servers/mcp-context7/README_ZH.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Context7 MCP Server
|
||||
|
||||
一个集成了[Context7](https://context7.com)的模型上下文协议(MCP)服务器实现,提供最新、版本特定的文档和代码示例。
|
||||
|
||||
源码地址:[https://github.com/upstash/context7](https://github.com/upstash/context7)
|
||||
|
||||
## 功能
|
||||
|
||||
- 获取最新、版本特定的文档
|
||||
- 从源码中提取真实可用的代码示例
|
||||
- 提供简洁、相关的信息,无冗余内容
|
||||
- 支持个人免费使用
|
||||
- 与MCP服务器和工具集成
|
||||
|
||||
## 使用教程
|
||||
|
||||
### 生成 SSE URL
|
||||
|
||||
在 MCP Server 界面,登录后输入 API-KEY,生成URL。
|
||||
|
||||
### 配置 MCP Client
|
||||
|
||||
在用户的 MCP Client 界面,将生成的 SSE URL添加到 MCP Server列表中。
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"context7": {
|
||||
"url": "https://mcp.higress.ai/mcp-context7/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 可用工具
|
||||
|
||||
#### resolve-library-id
|
||||
用于将通用包名解析为Context7兼容的库ID,是使用get-library-docs工具获取文档的必要前置步骤。
|
||||
|
||||
参数说明:
|
||||
- query: 要搜索的库名称,用于获取Context7兼容的库ID (必填)
|
||||
|
||||
#### get-library-docs
|
||||
获取库的最新文档。使用前必须先调用resolve-library-id工具获取Context7兼容的库ID。
|
||||
|
||||
参数说明:
|
||||
- folders: 用于组织文档的文件夹过滤器
|
||||
- libraryId: 库的唯一标识符 (必填)
|
||||
- tokens: 返回的最大token数,默认5000
|
||||
- topic: 文档中的特定主题
|
||||
- type: 要检索的文档类型,目前仅支持"txt"
|
||||
55
plugins/wasm-go/mcp-servers/mcp-context7/mcp-server.yaml
Normal file
55
plugins/wasm-go/mcp-servers/mcp-context7/mcp-server.yaml
Normal file
@@ -0,0 +1,55 @@
|
||||
server:
|
||||
name: context7-mcp-server
|
||||
tools:
|
||||
- name: resolve-library-id
|
||||
description: Required first step - Resolves a general package name into a Context7-compatible library ID. Must be called before using 'get-library-docs' to retrieve a valid Context7-compatible library ID.
|
||||
args:
|
||||
- name: query
|
||||
description: Library name to search for and retrieve a Context7-compatible library ID.
|
||||
type: string
|
||||
required: true
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: https://context7.com/api/v1/search
|
||||
method: GET
|
||||
responseTemplate:
|
||||
body: |
|
||||
{{- range $index, $item := .results }}
|
||||
## 结果 {{add $index 1}}
|
||||
- **id**: {{ $item.id }}
|
||||
- **title**: {{ $item.title }}
|
||||
- **description**: {{ $item.description }}
|
||||
{{- end }}
|
||||
- name: get-library-docs
|
||||
description: Fetches up-to-date documentation for a library. You must call 'resolve-library-id' first to obtain the exact Context7-compatible library ID required to use this tool.
|
||||
args:
|
||||
- name: folders
|
||||
description: Folders filter for organizing documentation
|
||||
type: string
|
||||
position: query
|
||||
- name: libraryId
|
||||
description: Unique identifier of the library
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
- name: tokens
|
||||
description: Maximum number of tokens to return
|
||||
type: integer
|
||||
position: query
|
||||
default: 5000
|
||||
- name: topic
|
||||
description: Specific topic within the documentation
|
||||
type: string
|
||||
position: query
|
||||
- name: type
|
||||
description: Type of documentation to retrieve
|
||||
type: string
|
||||
position: query
|
||||
enum: ["txt"]
|
||||
requestTemplate:
|
||||
url: https://context7.com/api/v1{libraryId}
|
||||
method: GET
|
||||
headers:
|
||||
- key: X-Context7-Source
|
||||
value: server
|
||||
|
||||
40
plugins/wasm-go/mcp-servers/mcp-e2bdev/README.md
Normal file
40
plugins/wasm-go/mcp-servers/mcp-e2bdev/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# E2BDev MCP Server
|
||||
|
||||
An implementation of the Model Context Protocol (MCP) server that integrates E2B Code Interpreter API, providing sandbox environment management capabilities, which enables execution of Python code.
|
||||
|
||||
|
||||
## Usage Guide
|
||||
|
||||
### Get API-KEY
|
||||
1. Register for an E2B account [Resigter Entry](https://e2b.dev/auth/sign-up). Each new account will receive 100 credits for free.
|
||||
2. Generate API Key in Dashboard [Manage API-KEY](https://e2b.dev/dashboard?tab=keys)
|
||||
|
||||
### Configure MCP Client
|
||||
|
||||
On the user's MCP Client interface, add E2BDev MCP Server configuration.
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"e2bdev": {
|
||||
"url": "https://mcp.higress.ai/mcp-e2bdev/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tools
|
||||
|
||||
- **create_sandbox**: Create E2B sandbox environment
|
||||
- Parameters:
|
||||
- timeout: Sandbox timeout in seconds, sandbox will be terminated after timeout
|
||||
- Returns: Sandbox ID
|
||||
|
||||
- **execute_code_sandbox**: Execute code in sandbox
|
||||
- Parameters:
|
||||
- sandbox_id: Sandbox ID, obtained from create_sandbox
|
||||
- code: Python code to execute
|
||||
- Returns: Execution result
|
||||
|
||||
- **kill_sandbox**: Terminate sandbox environment
|
||||
- Parameters:
|
||||
- sandbox_id: Sandbox ID to terminate
|
||||
- Returns: Termination result
|
||||
39
plugins/wasm-go/mcp-servers/mcp-e2bdev/README_ZH.md
Normal file
39
plugins/wasm-go/mcp-servers/mcp-e2bdev/README_ZH.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# E2BDev MCP Server
|
||||
|
||||
基于E2B Code Interpreter API 的MCP服务器实现,提供沙盒环境管理功能,沙盒环境中可执行Python代码。
|
||||
|
||||
## 使用教程
|
||||
|
||||
### 获取 API-KEY
|
||||
1. 注册E2B账号 [注册入口](https://e2b.dev/auth/sign-up),每位新用户有$100的免费额度。
|
||||
2. 在DashBoard中生成 API Key [生成 API Key](https://e2b.dev/dashboard?tab=keys)
|
||||
|
||||
### 配置 MCP Client
|
||||
|
||||
在用户的 MCP Client 界面,添加 E2BDev MCP Server 配置。
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"e2bdev": {
|
||||
"url": "https://mcp.higress.ai/mcp-e2bdev/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 工具使用
|
||||
|
||||
- **create_sandbox**: 创建E2B沙盒环境
|
||||
- 参数:
|
||||
- timeout: 沙盒超时时间(秒),超时后沙盒将被终止
|
||||
- 返回: 沙盒ID
|
||||
|
||||
- **execute_code_sandbox**: 在沙盒中执行代码
|
||||
- 参数:
|
||||
- sandbox_id: 沙盒ID,从create_sandbox获取
|
||||
- code: 要执行的Python代码
|
||||
- 返回: 执行结果
|
||||
|
||||
- **kill_sandbox**: 终止沙盒环境
|
||||
- 参数:
|
||||
- sandbox_id: 要终止的沙盒ID
|
||||
- 返回: 终止结果
|
||||
87
plugins/wasm-go/mcp-servers/mcp-e2bdev/mcp-server.yaml
Normal file
87
plugins/wasm-go/mcp-servers/mcp-e2bdev/mcp-server.yaml
Normal file
@@ -0,0 +1,87 @@
|
||||
server:
|
||||
name: e2bdev-api-server
|
||||
config:
|
||||
apiKey: ""
|
||||
tools:
|
||||
- name: create_sandbox
|
||||
description: Create e2b sandbox and return sandboxID
|
||||
args:
|
||||
- name: templateID
|
||||
description: "type of sandbox, fixed parameter"
|
||||
type: string
|
||||
enum: ["code-interpreter-beta"]
|
||||
- name: timeout
|
||||
description: "sanbox timeout in seconds, sanbox will be killed after timeout."
|
||||
type: int
|
||||
required: true
|
||||
default: 300
|
||||
|
||||
requestTemplate:
|
||||
url: https://api.e2b.dev/sandboxes
|
||||
method: POST
|
||||
argsToJsonBody: true
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: "application/json"
|
||||
- key: X-API-key
|
||||
value: "{{.config.apiKey}}"
|
||||
responseTemplate:
|
||||
body: |
|
||||
{
|
||||
"sandboxID": "{{.sandboxID}}-{{.clientID}}"
|
||||
}
|
||||
|
||||
- name: execute_code_sandbox
|
||||
description: Execute code in e2b sandbox
|
||||
args:
|
||||
- name: sandbox_id
|
||||
description: "create sandbox id, get from create_sandbox"
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
- name: code
|
||||
description: "python code to execute"
|
||||
type: string
|
||||
required: true
|
||||
position: body
|
||||
requestTemplate:
|
||||
url: "https://49999-{{.args.sandbox_id}}.e2b.dev/execute"
|
||||
method: POST
|
||||
argsToJsonBody: true
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: "application/json"
|
||||
- key: Authorization
|
||||
value: "Bearer {{.config.apiKey}}"
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
result.type is valid only when in ["stdout", "stdout", "result", "error"]
|
||||
- **result: **: (Type: object)
|
||||
- **result.type**: (Type: string)
|
||||
- **result.text**: (Type: string)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: kill_sandbox
|
||||
description: Kill e2b sandbox
|
||||
args:
|
||||
- name: sandbox_id
|
||||
description: "sandbox id, get from get_sandbox_id"
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
requestTemplate:
|
||||
url: https://api.e2b.dev/sandboxes/{{.args.sandbox_id}}
|
||||
method: DELETE
|
||||
headers:
|
||||
- key: X-API-key
|
||||
value: "{{.config.apiKey}}"
|
||||
@@ -27,7 +27,7 @@ On the user's MCP Client interface, add the generated SSE URL to the MCP Server
|
||||
```json
|
||||
"mcpServers": {
|
||||
"firecrawl": {
|
||||
"url": "http://mcp.higress.ai/mcp-firecrawl/{generate_key}",
|
||||
"url": "https://mcp.higress.ai/mcp-firecrawl/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
```json
|
||||
"mcpServers": {
|
||||
"firecrawl": {
|
||||
"url": "http://mcp.higress.ai/mcp-firecrawl/{generate_key}",
|
||||
"url": "https://mcp.higress.ai/mcp-firecrawl/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -33,7 +33,7 @@ On the user's MCP Client interface, add the generated SSE URL to the MCP Server
|
||||
```json
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"url": "http://mcp.higress.ai/mcp-github/{generate_key}",
|
||||
"url": "https://mcp.higress.ai/mcp-github/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -33,7 +33,7 @@ GitHub API 的 MCP 服务器实现,支持文件操作、仓库管理、搜索
|
||||
```json
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"url": "http://mcp.higress.ai/mcp-github/{generate_key}",
|
||||
"url": "https://mcp.higress.ai/mcp-github/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -52,6 +52,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: search_repositories
|
||||
description: 搜索GitHub仓库
|
||||
@@ -78,6 +80,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: create_repository
|
||||
description: 在您的账户中创建新的GitHub仓库
|
||||
@@ -115,6 +119,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: get_file_contents
|
||||
description: 从GitHub仓库获取文件或目录内容
|
||||
@@ -145,6 +151,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: push_files
|
||||
description: 在单个提交中推送多个文件到GitHub仓库
|
||||
@@ -192,6 +200,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: create_issue
|
||||
description: 在GitHub仓库创建新Issue
|
||||
@@ -246,6 +256,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: create_pull_request
|
||||
description: 在GitHub仓库创建新的Pull Request
|
||||
@@ -301,6 +313,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: fork_repository
|
||||
description: 将GitHub仓库fork到您的账户或指定组织
|
||||
@@ -331,6 +345,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: create_branch
|
||||
description: 在GitHub仓库创建新分支
|
||||
@@ -366,6 +382,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: list_commits
|
||||
description: 获取GitHub仓库分支的提交列表
|
||||
@@ -400,6 +418,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: list_issues
|
||||
description: 列出并过滤GitHub仓库的Issues
|
||||
@@ -452,6 +472,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: update_issue
|
||||
description: 更新GitHub仓库中的现有Issue
|
||||
@@ -515,6 +537,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: add_issue_comment
|
||||
description: 在GitHub Issue中添加评论
|
||||
@@ -549,6 +573,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: search_code
|
||||
description: 在GitHub仓库中搜索代码
|
||||
@@ -583,6 +609,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: search_issues
|
||||
description: 在GitHub仓库中搜索Issues和Pull Requests
|
||||
@@ -617,6 +645,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: search_users
|
||||
description: 在GitHub中搜索用户
|
||||
@@ -651,6 +681,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: get_issue
|
||||
description: 获取GitHub仓库中特定Issue的详细信息
|
||||
@@ -677,6 +709,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: get_pull_request
|
||||
description: 获取GitHub仓库中特定Pull Request的详细信息
|
||||
@@ -703,6 +737,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: list_pull_requests
|
||||
description: 列出并过滤GitHub仓库的Pull Requests
|
||||
@@ -753,6 +789,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: create_pull_request_review
|
||||
description: 在GitHub Pull Request上创建review
|
||||
@@ -814,6 +852,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: merge_pull_request
|
||||
description: 合并GitHub Pull Request
|
||||
@@ -858,6 +898,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: get_pull_request_files
|
||||
description: 获取GitHub Pull Request中更改的文件列表
|
||||
@@ -884,6 +926,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: get_pull_request_status
|
||||
description: 获取GitHub Pull Request的状态检查结果
|
||||
@@ -910,6 +954,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: update_pull_request_branch
|
||||
description: 使用base分支的最新更改更新Pull Request分支
|
||||
@@ -944,6 +990,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: get_pull_request_comments
|
||||
description: 获取GitHub Pull Request的review评论
|
||||
@@ -970,6 +1018,8 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
- name: get_pull_request_reviews
|
||||
description: 获取GitHub Pull Request的reviews
|
||||
@@ -996,3 +1046,5 @@ tools:
|
||||
value: "application/vnd.github+json"
|
||||
- key: X-GitHub-Api-Version
|
||||
value: "2022-11-28"
|
||||
- key: User-Agent
|
||||
value: "higress-mcp"
|
||||
|
||||
58
plugins/wasm-go/mcp-servers/mcp-librechat/README.md
Normal file
58
plugins/wasm-go/mcp-servers/mcp-librechat/README.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# LibreChat MCP Server
|
||||
|
||||
An implementation of the Librechat Code Interpreter MCP server that follows the OpenAPI specification, providing code execution and file management capabilities.
|
||||
|
||||
## Features
|
||||
|
||||
- Supports code execution in multiple programming languages
|
||||
- Supports file upload, download and deletion
|
||||
- Provides detailed API response information
|
||||
|
||||
## Usage Guide
|
||||
|
||||
### Get API-KEY
|
||||
1. Register for a LibreChat account [Visit official website](https://code.librechat.ai)
|
||||
2. Manage your plan and then generate API Key through developer console.
|
||||
|
||||
### Generate SSE URL
|
||||
|
||||
On the MCP Server interface, log in and enter the API-KEY to generate the URL.
|
||||
|
||||
### Configure MCP Client
|
||||
|
||||
On the user's MCP Client interface, add the generated SSE URL to the MCP Server list.
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"librechat": {
|
||||
"url": "https://mcp.higress.ai/mcp-librechat/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Available Tools
|
||||
|
||||
#### delete_file
|
||||
Delete specified file
|
||||
|
||||
Parameters:
|
||||
- fileId: File ID (required)
|
||||
- session_id: Session ID (required)
|
||||
|
||||
#### executeCode
|
||||
Execute code in specified programming language
|
||||
|
||||
Parameters:
|
||||
- code: Source code to execute (required)
|
||||
- lang: Programming language (required, options: c, cpp, d, f90, go, java, js, php, py, rs, ts, r)
|
||||
- args: Command line arguments (optional)
|
||||
- entity_id: Assistant/agent identifier (optional)
|
||||
- files: Array of file references (optional)
|
||||
- user_id: User identifier (optional)
|
||||
|
||||
#### get_file
|
||||
Get file information
|
||||
|
||||
Parameters:
|
||||
- session_id: Session ID (required)
|
||||
- detail: Detail information (optional)
|
||||
58
plugins/wasm-go/mcp-servers/mcp-librechat/README_ZH.md
Normal file
58
plugins/wasm-go/mcp-servers/mcp-librechat/README_ZH.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# LibreChat MCP Server
|
||||
|
||||
一个基于OpenAPI规范的 LibreChat Code Interpreter MCP 服务器,提供代码执行和文件管理功能。
|
||||
|
||||
## 功能
|
||||
|
||||
- 支持多种编程语言的代码执行
|
||||
- 支持文件上传、下载和删除
|
||||
- 提供详细的API响应信息
|
||||
|
||||
## 使用教程
|
||||
|
||||
### 获取 API-KEY
|
||||
1. 注册LibreChat账号 [访问官网](https://code.librechat.ai)
|
||||
2. 在控制台界面选择付费计划,并创建 API Key
|
||||
|
||||
### 生成 SSE URL
|
||||
|
||||
在 MCP Server 界面,登录后输入 API-KEY,生成URL。
|
||||
|
||||
### 配置 MCP Client
|
||||
|
||||
在用户的 MCP Client 界面,将生成的 SSE URL添加到 MCP Server列表中。
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"librechat": {
|
||||
"url": "https://mcp.higress.ai/mcp-librechat/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 可用工具
|
||||
|
||||
#### delete_file
|
||||
删除指定文件
|
||||
|
||||
参数:
|
||||
- fileId: 文件ID (必填)
|
||||
- session_id: 会话ID (必填)
|
||||
|
||||
#### executeCode
|
||||
执行指定编程语言的代码
|
||||
|
||||
参数:
|
||||
- code: 要执行的源代码 (必填)
|
||||
- lang: 编程语言 (必填,可选值:c, cpp, d, f90, go, java, js, php, py, rs, ts, r)
|
||||
- args: 命令行参数 (可选)
|
||||
- entity_id: 助手/代理标识符 (可选)
|
||||
- files: 文件引用数组 (可选)
|
||||
- user_id: 用户标识符 (可选)
|
||||
|
||||
#### get_file
|
||||
获取文件信息
|
||||
|
||||
参数:
|
||||
- session_id: 会话ID (必填)
|
||||
- detail: 详细信息 (可选)
|
||||
145
plugins/wasm-go/mcp-servers/mcp-librechat/mcp-server.yaml
Normal file
145
plugins/wasm-go/mcp-servers/mcp-librechat/mcp-server.yaml
Normal file
@@ -0,0 +1,145 @@
|
||||
server:
|
||||
name: librechat-api-server
|
||||
config:
|
||||
apiKey: ""
|
||||
tools:
|
||||
- name: delete_file
|
||||
description: Delete a file
|
||||
args:
|
||||
- name: fileId
|
||||
description: ""
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
- name: session_id
|
||||
description: ""
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
requestTemplate:
|
||||
url: https://api.librechat.ai/v1/files/{session_id}/{fileId}
|
||||
method: DELETE
|
||||
headers:
|
||||
- key: x-api-key
|
||||
value: "{{ .config.apiKey }}"
|
||||
responseTemplate: {}
|
||||
- name: executeCode
|
||||
description: Execute code - Execute code with specified language and parameters
|
||||
args:
|
||||
- name: args
|
||||
description: Optional command line arguments to pass to the program
|
||||
type: string
|
||||
position: body
|
||||
- name: code
|
||||
description: The source code to be executed
|
||||
type: string
|
||||
required: true
|
||||
position: body
|
||||
- name: entity_id
|
||||
description: Optional assistant/agent identifier for file sharing and reference. Must be a valid nanoid-compatible string.
|
||||
type: string
|
||||
position: body
|
||||
- name: files
|
||||
description: Array of file references to be used during execution
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
position: body
|
||||
- name: lang
|
||||
description: The programming language of the code
|
||||
type: string
|
||||
required: true
|
||||
enum: ["c","cpp","d","f90","go","java","js","php","py","rs","ts","r"]
|
||||
position: body
|
||||
- name: user_id
|
||||
description: Optional user identifier
|
||||
type: string
|
||||
position: body
|
||||
requestTemplate:
|
||||
url: https://api.librechat.ai/v1/exec
|
||||
method: POST
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: x-api-key
|
||||
value: "{{ .config.apiKey }}"
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **files**: (Type: array)
|
||||
- **files[].id**: (Type: string)
|
||||
- **files[].name**: (Type: string)
|
||||
- **files[].path**: (Type: string)
|
||||
- **language**: (Type: string)
|
||||
- **run**: (Type: object)
|
||||
- **run.code**: (Type: integer)
|
||||
- **run.cpu_time**: (Type: number)
|
||||
- **run.memory**: (Type: integer)
|
||||
- **run.message**: (Type: string)
|
||||
- **run.output**: (Type: string)
|
||||
- **run.signal**: (Type: string)
|
||||
- **run.status**: (Type: string)
|
||||
- **run.stderr**: (Type: string)
|
||||
- **run.stdout**: (Type: string)
|
||||
- **run.wall_time**: (Type: number)
|
||||
- **session_id**: (Type: string)
|
||||
- **version**: (Type: string)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: get_file
|
||||
description: Get files information
|
||||
args:
|
||||
- name: detail
|
||||
description: ""
|
||||
type: string
|
||||
position: query
|
||||
- name: session_id
|
||||
description: ""
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
requestTemplate:
|
||||
url: https://api.librechat.ai/v1/files/{session_id}
|
||||
method: GET
|
||||
headers:
|
||||
- key: x-api-key
|
||||
value: "{{ .config.apiKey }}"
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **items**: Array of items (Type: array)
|
||||
- **items.content**: (Type: string)
|
||||
- **items.contentType**: (Type: string)
|
||||
- **items.etag**: (Type: string)
|
||||
- **items.id**: (Type: string)
|
||||
- **items.lastModified**: (Type: string)
|
||||
- **items.metadata**: (Type: object)
|
||||
- **items.metadata.content-type**: (Type: string)
|
||||
- **items.metadata.original-filename**: (Type: string)
|
||||
- **items.name**: (Type: string)
|
||||
- **items.session_id**: (Type: string)
|
||||
- **items.size**: (Type: number)
|
||||
|
||||
## Original Response
|
||||
|
||||
@@ -32,7 +32,7 @@ On the user's MCP Client interface, add the generated SSE URL to the MCP Server
|
||||
```json
|
||||
"mcpServers": {
|
||||
"notion": {
|
||||
"url": "http://mcp.higress.ai/mcp-notion/{generate_key}",
|
||||
"url": "https://mcp.higress.ai/mcp-notion/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -32,7 +32,7 @@ Notion MCP Server 提供了以下功能:
|
||||
```json
|
||||
"mcpServers": {
|
||||
"notion": {
|
||||
"url": "http://mcp.higress.ai/mcp-notion/{generate_key}",
|
||||
"url": "https://mcp.higress.ai/mcp-notion/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -17,7 +17,7 @@ On the user's MCP Client interface, add the generated SSE URL to the MCP Server
|
||||
```json
|
||||
"mcpServers": {
|
||||
"weather": {
|
||||
"url": "http://mcp.higress.ai/mcp-weather/{generate_key}",
|
||||
"url": "https://mcp.higress.ai/mcp-weather/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
```json
|
||||
"mcpServers": {
|
||||
"weather": {
|
||||
"url": "http://mcp.higress.ai/mcp-weather/{generate_key}",
|
||||
"url": "https://mcp.higress.ai/mcp-weather/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
32
plugins/wasm-go/mcp-servers/mcp-wolframalpha/README.md
Normal file
32
plugins/wasm-go/mcp-servers/mcp-wolframalpha/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# WolframAlpha MCP Server
|
||||
|
||||
An implementation of the Model Context Protocol (MCP) server that integrates [WolframAlpha](https://www.wolframalpha.com/), providing natural language computation and knowledge query capabilities.
|
||||
|
||||
## Features
|
||||
|
||||
- Supports natural language queries in mathematics, physics, chemistry, geography, history, art, astronomy, and more
|
||||
- Performs mathematical calculations, date and unit conversions, formula solving, etc.
|
||||
- Supports image result display
|
||||
- Automatically converts complex queries into simplified keyword queries
|
||||
- Supports multilingual queries (automatically translates to English for processing, returns results in original language)
|
||||
|
||||
## Usage Guide
|
||||
|
||||
### Get AppID
|
||||
1. Register for a WolframAlpha developer account [Create a Wolfram ID](https://account.wolfram.com/login/create)
|
||||
2. Generate LLM-API AppID [Get An App ID](https://developer.wolframalpha.com/access)
|
||||
|
||||
### Generate SSE URL
|
||||
|
||||
On the MCP Server interface, log in and enter the AppID to generate the URL.
|
||||
|
||||
### Configure MCP Client
|
||||
|
||||
On the user's MCP Client interface, add the generated SSE URL to the MCP Server list.
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"wolframalpha": {
|
||||
"url": "https://mcp.higress.ai/mcp-wolframalpha/{generate_key}",
|
||||
}
|
||||
}
|
||||
32
plugins/wasm-go/mcp-servers/mcp-wolframalpha/README_ZH.md
Normal file
32
plugins/wasm-go/mcp-servers/mcp-wolframalpha/README_ZH.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# WolframAlpha MCP Server
|
||||
|
||||
一个集成了[WolframAlpha](https://www.wolframalpha.com/)的模型上下文协议(MCP)服务器实现,提供自然语言计算和知识查询功能。
|
||||
|
||||
## 功能
|
||||
|
||||
- 支持自然语言查询,涵盖数学、物理、化学、地理、历史、艺术、天文等领域
|
||||
- 执行数学计算、日期和单位转换、公式求解等
|
||||
- 支持图像结果展示
|
||||
- 自动将复杂查询转换为简化关键词查询
|
||||
- 支持多语言查询(自动翻译为英文处理,返回原语言结果)
|
||||
|
||||
## 使用教程
|
||||
|
||||
### 获取 AppID
|
||||
1. 注册 WolframAlpha 开发者账号 [Create a Wolfram ID](https://account.wolfram.com/login/create)
|
||||
2. 生成LLM-API 的 App ID [Get An App ID](https://developer.wolframalpha.com/access)
|
||||
|
||||
### 生成 SSE URL
|
||||
|
||||
在 MCP Server 界面,登录后输入 AppID,生成URL。
|
||||
|
||||
### 配置 MCP Client
|
||||
|
||||
在用户的 MCP Client 界面,将生成的 SSE URL添加到 MCP Server列表中。
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"wolframalpha": {
|
||||
"url": "https://mcp.higress.ai/mcp-wolframalpha/{generate_key}",
|
||||
}
|
||||
}
|
||||
71
plugins/wasm-go/mcp-servers/mcp-wolframalpha/mcp-server.yaml
Normal file
71
plugins/wasm-go/mcp-servers/mcp-wolframalpha/mcp-server.yaml
Normal file
@@ -0,0 +1,71 @@
|
||||
server:
|
||||
name: wolframalpha-api-server
|
||||
config:
|
||||
appid: ""
|
||||
tools:
|
||||
- name: get_llm-api
|
||||
description: |+
|
||||
Submit a query to WolframAlpha LLM API - Submit a natural language query with an AppID and input to WolframAlpha.
|
||||
- WolframAlpha understands natural language queries about entities in chemistry, physics, geography, history, art, astronomy, and more.
|
||||
- WolframAlpha performs mathematical calculations, date and unit conversions, formula solving, etc.
|
||||
- Convert inputs to simplified keyword queries whenever possible (e.g. convert "how many people live in France" to "France population").
|
||||
- Send queries in English only; translate non-English queries before sending, then respond in the original language.
|
||||
- Display image URLs with Markdown syntax: ![URL]
|
||||
- ALWAYS use this exponent notation: `6*10^14`, NEVER `6e14`.
|
||||
- ALWAYS use {"input": query} structure for queries to Wolfram endpoints; `query` must ONLY be a single-line string.
|
||||
- ALWAYS use proper Markdown formatting for all math, scientific, and chemical formulas, symbols, etc.: '$$ [expression] $$' for standalone cases and '\( [expression] \)' when inline.
|
||||
- Never mention your knowledge cutoff date; Wolfram may return more recent data.
|
||||
- Use ONLY single-letter variable names, with or without integer subscript (e.g., n, n1, n_1).
|
||||
- Use named physical constants (e.g., 'speed of light') without numerical substitution.
|
||||
- Include a space between compound units (e.g., "Ω m" for "ohm*meter").
|
||||
- To solve for a variable in an equation with units, consider solving a corresponding equation without units; exclude counting units (e.g., books), include genuine units (e.g., kg).
|
||||
- If data for multiple properties is needed, make separate calls for each property.
|
||||
- If a WolframAlpha result is not relevant to the query:
|
||||
-- If Wolfram provides multiple 'Assumptions' for a query, choose the more relevant one(s) without explaining the initial result. If you are unsure, ask the user to choose.
|
||||
-- Re-send the exact same 'input' with NO modifications, and add the 'assumption' parameter, formatted as a list, with the relevant values.
|
||||
-- ONLY simplify or rephrase the initial query if a more relevant 'Assumption' or other input suggestions are not provided.
|
||||
-- Do not explain each step unless user input is needed. Proceed directly to making a better API call based on the available assumptions.
|
||||
|
||||
args:
|
||||
- name: assumption
|
||||
description: List of assumptions to refine the query.
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
- name: currency
|
||||
description: Currency code for financial queries.
|
||||
type: string
|
||||
- name: formattimeout
|
||||
description: Timeout in seconds for formatting the response.
|
||||
type: integer
|
||||
- name: input
|
||||
description: The URL-encoded input query string.
|
||||
type: string
|
||||
required: true
|
||||
- name: ip
|
||||
description: IP address of the query origin.
|
||||
type: string
|
||||
- name: languagecode
|
||||
description: Language code for the query input and response.
|
||||
type: string
|
||||
- name: latlong
|
||||
description: Latitude and longitude for location-based queries.
|
||||
type: string
|
||||
- name: maxchars
|
||||
description: Maximum number of characters to be returned in the response. Defaults to 6800 characters.
|
||||
type: integer
|
||||
- name: timezone
|
||||
description: Timezone for the query.
|
||||
type: string
|
||||
- name: units
|
||||
description: Preferred units for result data (e.g., metric or imperial).
|
||||
type: string
|
||||
requestTemplate:
|
||||
argsToUrlParam: true
|
||||
url: https://www.wolframalpha.com/api/v1/llm-api
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: "Bearer {{.config.appid}}"
|
||||
|
||||
|
||||
37
plugins/wasm-go/mcp-servers/mcp-yuque/README.md
Normal file
37
plugins/wasm-go/mcp-servers/mcp-yuque/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Yuque MCP Server
|
||||
|
||||
Implementation of the MCP server based on the Yuque Open Service API, enabling the editing, updating, and publishing of Yuque knowledge bases and documents through the MCP protocol.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
Currently supports the following operations:
|
||||
- **Knowledge Base Management**: Create, search, update, delete knowledge bases, etc.
|
||||
- **Document Management**: Create, update, view history details, search documents, etc.
|
||||
|
||||
For enterprise team users:
|
||||
- **Member Management**: Manage knowledge base members and permissions.
|
||||
- **Data Aggregation**: Statistics on knowledge bases, documents, members, etc.
|
||||
|
||||
|
||||
## Usage Guide
|
||||
|
||||
### Get AccessToken
|
||||
|
||||
Refer to the [Yuque Developer Documentation](https://www.yuque.com/yuque/developer/api) for personal user authentication or enterprise team identity authentication.
|
||||
|
||||
### Generate SSE URL
|
||||
|
||||
On the MCP Server interface, log in and enter the AccessToken to generate the URL.
|
||||
|
||||
### Configure MCP Client
|
||||
|
||||
On the user's MCP Client interface, add the generated SSE URL to the MCP Server list.
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"yuque": {
|
||||
"url": "https://mcp.higress.ai/mcp-yuque/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
38
plugins/wasm-go/mcp-servers/mcp-yuque/README_ZH.md
Normal file
38
plugins/wasm-go/mcp-servers/mcp-yuque/README_ZH.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# 语雀 MCP Server
|
||||
|
||||
基于语雀开放服务 API 的 MCP 服务器实现,通过 MCP 协议,实现语雀知识库、文档的编辑、更新发布。
|
||||
|
||||
## 功能
|
||||
|
||||
当前支持以下操作:
|
||||
- **知识库管理**:新建、搜索、更新、删除知识库等。
|
||||
- **文档管理**:创建、更新、历史详情、搜索文档等。
|
||||
|
||||
|
||||
对于企业团队用户:
|
||||
- **成员管理**:管理知识库成员、权限。
|
||||
- **数据汇总**:知识库、文档、成员等数据统计。
|
||||
|
||||
|
||||
## 使用教程
|
||||
|
||||
### 获取 AccessToken
|
||||
|
||||
参考[语雀开发者文档](https://www.yuque.com/yuque/developer/api),进行个人用户认证或企业团队身份认证。
|
||||
|
||||
### 生成 SSE URL
|
||||
|
||||
在 MCP Server 界面,登录后输入 AccessToken,生成URL。
|
||||
|
||||
### 配置 MCP Client
|
||||
|
||||
在用户的 MCP Client 界面,将生成的 SSE URL添加到 MCP Server列表中。
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"yuque": {
|
||||
"url": "https://mcp.higress.ai/mcp-yuque/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1037
plugins/wasm-go/mcp-servers/mcp-yuque/mcp-server.yaml
Normal file
1037
plugins/wasm-go/mcp-servers/mcp-yuque/mcp-server.yaml
Normal file
File diff suppressed because one or more lines are too long
@@ -202,6 +202,7 @@ pub struct AiDataMaskingConfig {
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
struct Message {
|
||||
#[serde(default)]
|
||||
content: String,
|
||||
}
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
@@ -221,8 +222,11 @@ struct ResMessage {
|
||||
|
||||
#[derive(Default, Debug, Deserialize, Serialize, Clone)]
|
||||
struct Usage {
|
||||
#[serde(default)]
|
||||
completion_tokens: i32,
|
||||
#[serde(default)]
|
||||
prompt_tokens: i32,
|
||||
#[serde(default)]
|
||||
total_tokens: i32,
|
||||
}
|
||||
|
||||
|
||||
@@ -299,7 +299,9 @@ data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012
|
||||
|
||||
data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"。"},"finish_reason":null}]}
|
||||
|
||||
data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop"}]}
|
||||
data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{}}
|
||||
|
||||
data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{}}],"usage":{"prompt_tokens":372,"completion_tokens":9,"total_tokens":381}}
|
||||
|
||||
data: [DONE]
|
||||
|
||||
|
||||
155
registry/mcp_model.go
Normal file
155
registry/mcp_model.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package registry
|
||||
|
||||
const (
|
||||
JsonGoTemplateType = "json-go-template"
|
||||
|
||||
IstioMcpAutoGeneratedPrefix = "istio-autogenerated-mcp"
|
||||
IstioMcpAutoGeneratedVsName = IstioMcpAutoGeneratedPrefix + "-vs"
|
||||
IstioMcpAutoGeneratedSeName = IstioMcpAutoGeneratedPrefix + "-se"
|
||||
IstioMcpAutoGeneratedDrName = IstioMcpAutoGeneratedPrefix + "-dr"
|
||||
IstioMcpAutoGeneratedHttpRouteName = IstioMcpAutoGeneratedPrefix + "-httproute"
|
||||
|
||||
DefaultMcpToolsGroup = "mcp-tools"
|
||||
DefaultMcpCredentialsGroup = "credentials"
|
||||
DefaultNacosServiceNamespace = "public"
|
||||
|
||||
StdioProtocol = "stdio"
|
||||
HttpProtocol = "http"
|
||||
DubboProtocol = "dubbo"
|
||||
McpSSEProtocol = "mcp-sse"
|
||||
McpStreambleProtocol = "mcp-streamble"
|
||||
)
|
||||
|
||||
type McpToolArgsType string
|
||||
|
||||
// WasmPluginConfig Struct for mcp tool wasm plugin marshal
|
||||
type WasmPluginConfig struct {
|
||||
Rules []*McpServerRule `json:"_rules_"`
|
||||
}
|
||||
|
||||
type McpServerRule struct {
|
||||
MatchRoute []string `json:"_match_route_,omitempty"`
|
||||
Server *ServerConfig `json:"server"`
|
||||
Tools []*McpTool `json:"tools"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Config map[string]interface{} `json:"config,omitempty"`
|
||||
AllowTools []string `json:"allowTools,omitempty"`
|
||||
}
|
||||
|
||||
type McpTool struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Args []*ToolArgs `json:"args,omitempty"`
|
||||
RequestTemplate *RequestTemplate `json:"requestTemplate"`
|
||||
ResponseTemplate *ResponseTemplate `json:"responseTemplate"`
|
||||
}
|
||||
|
||||
type ToolArgs struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Required bool `json:"required,omitempty"`
|
||||
Default interface{} `json:"default,omitempty"`
|
||||
Enum []interface{} `json:"enum,omitempty"`
|
||||
Items []interface{} `json:"items,omitempty"`
|
||||
Properties interface{} `json:"properties,omitempty"`
|
||||
Position string `json:"position,omitempty"`
|
||||
}
|
||||
|
||||
type RequestTemplate struct {
|
||||
URL string `json:"url"`
|
||||
Method string `json:"method"`
|
||||
Headers []*RequestTemplateHeaders `json:"headers,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
ArgsToJsonBody bool `json:"argsToJsonBody,omitempty"`
|
||||
ArgsToUrlParam bool `json:"argsToUrlParam,omitempty"`
|
||||
ArgsToFormBody bool `json:"argsToFormBody,omitempty"`
|
||||
}
|
||||
|
||||
type RequestTemplateHeaders struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type ResponseTemplate struct {
|
||||
Body string `json:"body,omitempty"`
|
||||
PrependBody string `json:"prependBody,omitempty"`
|
||||
AppendBody string `json:"appendBody,omitempty"`
|
||||
}
|
||||
|
||||
// McpServer Struct for mcp server json unmarshal
|
||||
type McpServer struct {
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
RemoteServerConfig *RemoteServerConfig `json:"remoteServerConfig,omitempty"`
|
||||
Credentials map[string]*CredentialRef `json:"credentials,omitempty"`
|
||||
ToolsDescriptionRef string `json:"toolsDescriptionRef,omitempty"`
|
||||
PromptDescriptionRef string `json:"promptDescriptionRef,omitempty"`
|
||||
ResourceDescriptionRef string `json:"resourceDescriptionRef,omitempty"`
|
||||
}
|
||||
|
||||
type RemoteServerConfig struct {
|
||||
ServiceRef *ServiceRef `json:"serviceRef,omitempty"`
|
||||
ExportPath string `json:"exportPath,omitempty"`
|
||||
BackendProtocol string `json:"backendProtocol,omitempty"`
|
||||
}
|
||||
|
||||
type CredentialRef struct {
|
||||
Ref string `json:"ref,omitempty"`
|
||||
}
|
||||
|
||||
type ServiceRef struct {
|
||||
NamespaceId string `json:"namespaceId,omitempty"`
|
||||
GroupName string `json:"groupName,omitempty"`
|
||||
ServiceName string `json:"serviceName,omitempty"`
|
||||
}
|
||||
|
||||
// McpToolConfig Struct for mcp tool json unmarshal
|
||||
type McpToolConfig struct {
|
||||
Tools []*ToolDescription `json:"tools,omitempty"`
|
||||
ToolsMeta map[string]*ToolsMeta `json:"toolsMeta,omitempty"`
|
||||
}
|
||||
|
||||
type ToolDescription struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
InputSchema InputSchema `json:"inputSchema"`
|
||||
}
|
||||
|
||||
type InputSchema struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Properties map[string]interface{} `json:"properties,omitempty"`
|
||||
Required []string `json:"required,omitempty"`
|
||||
}
|
||||
|
||||
type ToolsMeta struct {
|
||||
InvokeContext map[string]string `json:"InvokeContext,omitempty"`
|
||||
Enabled bool `json:"Enabled,omitempty"`
|
||||
Templates map[string]interface{} `json:"Templates,omitempty"`
|
||||
}
|
||||
|
||||
type JsonGoTemplate struct {
|
||||
RequestTemplate RequestTemplate `json:"requestTemplate"`
|
||||
ResponseTemplate ResponseTemplate `json:"responseTemplate"`
|
||||
ArgsPosition map[string]string `json:"argsPosition,omitempty"`
|
||||
}
|
||||
@@ -15,12 +15,21 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
higressconfig "github.com/alibaba/higress/pkg/config"
|
||||
"github.com/alibaba/higress/registry"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
extensions "istio.io/api/extensions/v1alpha1"
|
||||
"istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/pkg/log"
|
||||
|
||||
"github.com/alibaba/higress/pkg/common"
|
||||
@@ -30,6 +39,8 @@ import (
|
||||
type Cache interface {
|
||||
UpdateServiceWrapper(service string, data *ServiceWrapper)
|
||||
DeleteServiceWrapper(service string)
|
||||
UpdateConfigCache(kind config.GroupVersionKind, key string, config *config.Config, forceDelete bool)
|
||||
GetAllConfigs(kind config.GroupVersionKind) map[string]*config.Config
|
||||
PurgeStaleService()
|
||||
UpdateServiceEntryEndpointWrapper(service, ip, regionId, zoneId, protocol string, labels map[string]string)
|
||||
GetServiceByEndpoints(requestVersions, endpoints map[string]bool, versionKey string, protocol common.Protocol) map[string][]string
|
||||
@@ -44,6 +55,7 @@ func NewCache() Cache {
|
||||
return &store{
|
||||
mux: &sync.RWMutex{},
|
||||
sew: make(map[string]*ServiceWrapper),
|
||||
configs: make(map[string]map[string]*config.Config),
|
||||
toBeUpdated: make([]*ServiceWrapper, 0),
|
||||
toBeDeleted: make([]*ServiceWrapper, 0),
|
||||
ip2services: make(map[string]map[string]bool),
|
||||
@@ -54,12 +66,85 @@ func NewCache() Cache {
|
||||
type store struct {
|
||||
mux *sync.RWMutex
|
||||
sew map[string]*ServiceWrapper
|
||||
configs map[string]map[string]*config.Config
|
||||
toBeUpdated []*ServiceWrapper
|
||||
toBeDeleted []*ServiceWrapper
|
||||
ip2services map[string]map[string]bool
|
||||
deferedDelete map[string]struct{}
|
||||
}
|
||||
|
||||
func (s *store) GetAllConfigs(kind config.GroupVersionKind) map[string]*config.Config {
|
||||
s.mux.Lock()
|
||||
defer s.mux.Unlock()
|
||||
cfgs, exist := s.configs[kind.String()]
|
||||
if !exist {
|
||||
return map[string]*config.Config{}
|
||||
}
|
||||
if kind == gvk.WasmPlugin {
|
||||
pluginConfig := ®istry.WasmPluginConfig{}
|
||||
var ns string
|
||||
for _, cfg := range cfgs {
|
||||
ns = cfg.Namespace
|
||||
rule := cfg.Spec.(*registry.McpServerRule)
|
||||
pluginConfig.Rules = append(pluginConfig.Rules, rule)
|
||||
}
|
||||
rulesBytes, err := json.Marshal(pluginConfig)
|
||||
if err != nil {
|
||||
log.Errorf("marshal mcp wasm plugin config error %v", err)
|
||||
return map[string]*config.Config{}
|
||||
}
|
||||
pbs := &structpb.Struct{}
|
||||
if err = protojson.Unmarshal(rulesBytes, pbs); err != nil {
|
||||
log.Errorf("unmarshal mcp wasm plugin config error %v", err)
|
||||
return map[string]*config.Config{}
|
||||
}
|
||||
wasmPlugin := &extensions.WasmPlugin{
|
||||
ImagePullPolicy: extensions.PullPolicy_Always,
|
||||
Phase: extensions.PluginPhase_UNSPECIFIED_PHASE,
|
||||
Priority: &wrapperspb.Int32Value{Value: 30},
|
||||
PluginConfig: pbs,
|
||||
Url: higressconfig.McpServerWasmImageUrl,
|
||||
}
|
||||
|
||||
return map[string]*config.Config{"wasm": &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.WasmPlugin,
|
||||
Name: "istio-autogenerated-mcp-wasmplugin",
|
||||
Namespace: ns,
|
||||
},
|
||||
Spec: wasmPlugin,
|
||||
}}
|
||||
}
|
||||
|
||||
return cfgs
|
||||
}
|
||||
|
||||
func (s *store) UpdateConfigCache(kind config.GroupVersionKind, key string, cfg *config.Config, forceDelete bool) {
|
||||
if cfg == nil && !forceDelete {
|
||||
return
|
||||
}
|
||||
|
||||
s.mux.Lock()
|
||||
if forceDelete {
|
||||
for _, allConfigs := range s.configs {
|
||||
delete(allConfigs, key)
|
||||
}
|
||||
log.Infof("Delete config %s in cache", key)
|
||||
} else {
|
||||
if _, exist := s.configs[kind.String()]; !exist {
|
||||
s.configs[kind.String()] = make(map[string]*config.Config)
|
||||
}
|
||||
|
||||
if _, exist := s.configs[kind.String()][key]; exist {
|
||||
log.Infof("Update kind %s config %s", kind.String(), key)
|
||||
} else {
|
||||
log.Infof("Add kind %s config %s", kind.String(), key)
|
||||
}
|
||||
s.configs[kind.String()][key] = cfg
|
||||
}
|
||||
s.mux.Unlock()
|
||||
}
|
||||
|
||||
func (s *store) UpdateServiceEntryEndpointWrapper(service, ip, regionId, zoneId, protocol string, labels map[string]string) {
|
||||
s.mux.Lock()
|
||||
defer s.mux.Unlock()
|
||||
@@ -226,6 +311,15 @@ func (s *store) GetAllDestinationRuleWrapper() []*ingress.WrapperDestinationRule
|
||||
drwList = append(drwList, serviceEntryWrapper.DeepCopy().DestinationRuleWrapper)
|
||||
}
|
||||
}
|
||||
configFromMcp := s.configs[gvk.DestinationRule.String()]
|
||||
for _, cfg := range configFromMcp {
|
||||
dr := cfg.Spec.(*v1alpha3.DestinationRule)
|
||||
drwList = append(drwList, &ingress.WrapperDestinationRule{
|
||||
DestinationRule: dr,
|
||||
ServiceKey: ingress.ServiceKey{ServiceFQDN: dr.Host},
|
||||
})
|
||||
}
|
||||
|
||||
return drwList
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user