mirror of
https://github.com/alibaba/higress.git
synced 2026-06-09 12:47:28 +08:00
fix: refactored mcp server auto discovery logic and fix some issue (#2382)
Co-authored-by: johnlanni <zty98751@alibaba-inc.com>
This commit is contained in:
585
registry/nacos/mcpserver/watcher_test.go
Normal file
585
registry/nacos/mcpserver/watcher_test.go
Normal file
@@ -0,0 +1,585 @@
|
||||
// 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 mcpserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
apiv1 "github.com/alibaba/higress/api/networking/v1"
|
||||
common2 "github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
provider "github.com/alibaba/higress/registry"
|
||||
"github.com/alibaba/higress/registry/memory"
|
||||
"github.com/nacos-group/nacos-sdk-go/v2/model"
|
||||
"github.com/stretchr/testify/mock"
|
||||
wrappers "google.golang.org/protobuf/types/known/wrapperspb"
|
||||
"istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/constants"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
)
|
||||
|
||||
type mockWatcher struct {
|
||||
watcher
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func newTestWatcher(cache memory.Cache, opts ...WatcherOption) mockWatcher {
|
||||
w := &watcher{
|
||||
watchingConfig: make(map[string]bool),
|
||||
RegistryType: "mcpserver",
|
||||
Status: provider.UnHealthy,
|
||||
cache: cache,
|
||||
mutex: &sync.Mutex{},
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
|
||||
w.NacosRefreshInterval = int64(DefaultRefreshInterval)
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(w)
|
||||
}
|
||||
|
||||
if w.NacosNamespace == "" {
|
||||
w.NacosNamespace = w.NacosNamespaceId
|
||||
}
|
||||
|
||||
return mockWatcher{watcher: *w, Mock: mock.Mock{}}
|
||||
}
|
||||
|
||||
func testCallback(msc *McpServerConfig) memory.Cache {
|
||||
registryConfig := &apiv1.RegistryConfig{
|
||||
Type: string(provider.Nacos),
|
||||
Name: "mse-nacos-public",
|
||||
Domain: "",
|
||||
Port: 8848,
|
||||
NacosAddressServer: "",
|
||||
NacosAccessKey: "ak",
|
||||
NacosSecretKey: "sk",
|
||||
NacosNamespaceId: "",
|
||||
NacosNamespace: "public",
|
||||
NacosGroups: []string{"dev"},
|
||||
NacosRefreshInterval: 0,
|
||||
EnableMCPServer: wrappers.Bool(true),
|
||||
McpServerExportDomains: []string{"mcp.com"},
|
||||
McpServerBaseUrl: "/mcp-servers/",
|
||||
EnableScopeMcpServers: wrappers.Bool(true),
|
||||
AllowMcpServers: []string{"mcp-server-1", "mcp-server-2"},
|
||||
Metadata: map[string]*apiv1.InnerMap{
|
||||
"routeName": &apiv1.InnerMap{
|
||||
InnerMap: map[string]string{"mcp-server-1": "mcp-route-1", "mcp-server-2": "mcp-route-2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
localCache := memory.NewCache()
|
||||
|
||||
testWatcher := newTestWatcher(localCache,
|
||||
WithType(registryConfig.Type),
|
||||
WithName(registryConfig.Name),
|
||||
WithNacosAddressServer(registryConfig.NacosAddressServer),
|
||||
WithDomain(registryConfig.Domain),
|
||||
WithPort(registryConfig.Port),
|
||||
WithNacosNamespaceId(registryConfig.NacosNamespaceId),
|
||||
WithNacosNamespace(registryConfig.NacosNamespace),
|
||||
WithNacosGroups(registryConfig.NacosGroups),
|
||||
WithNacosAccessKey(registryConfig.NacosAccessKey),
|
||||
WithNacosSecretKey(registryConfig.NacosSecretKey),
|
||||
WithNacosRefreshInterval(registryConfig.NacosRefreshInterval),
|
||||
WithMcpExportDomains(registryConfig.McpServerExportDomains),
|
||||
WithMcpBaseUrl(registryConfig.McpServerBaseUrl),
|
||||
WithEnableMcpServer(registryConfig.EnableMCPServer))
|
||||
testWatcher.AppendServiceUpdateHandler(func() {
|
||||
fmt.Println("testWatcher service update success")
|
||||
})
|
||||
|
||||
callback := testWatcher.mcpServerListener("mock-data-id")
|
||||
callback(msc)
|
||||
return localCache
|
||||
}
|
||||
|
||||
func Test_Watcher(t *testing.T) {
|
||||
dataId := "mock-data-id"
|
||||
|
||||
testCase := []struct {
|
||||
name string
|
||||
msc *McpServerConfig
|
||||
dataId string
|
||||
wantConfig map[string]*config.Config
|
||||
}{
|
||||
{
|
||||
name: "normal case",
|
||||
dataId: dataId,
|
||||
msc: &McpServerConfig{
|
||||
Credentials: map[string]interface{}{
|
||||
"test-server": map[string]string{"data": "value"},
|
||||
},
|
||||
ServiceInfo: &model.Service{
|
||||
Hosts: []model.Instance{
|
||||
{
|
||||
Ip: "127.0.0.1",
|
||||
Port: 8080,
|
||||
Metadata: map[string]string{"protocol": "http"},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServerSpecConfig: `{
|
||||
"name": "explore",
|
||||
"protocol": "http",
|
||||
"description": "explore",
|
||||
"remoteServerConfig": {
|
||||
"serviceRef": {
|
||||
"namespaceId": "public",
|
||||
"groupName": "DEFAULT_GROUP",
|
||||
"serviceName": "explore"
|
||||
},
|
||||
"exportPath": ""
|
||||
},
|
||||
"enabled": true
|
||||
}`,
|
||||
ToolsSpecConfig: `{
|
||||
"tools": [
|
||||
{
|
||||
"name": "explore",
|
||||
"description": "find name from tag",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "string",
|
||||
"description": "tag"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"toolsMeta": {
|
||||
"explore": {
|
||||
"enabled": true,
|
||||
"templates": {
|
||||
"json-go-template": {
|
||||
"requestTemplate": {
|
||||
"method": "GET",
|
||||
"url": "/v0/explore",
|
||||
"argsToUrlParam": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
wantConfig: map[string]*config.Config{
|
||||
gvk.ServiceEntry.String(): &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.ServiceEntry,
|
||||
Name: fmt.Sprintf("%s-%s", provider.IstioMcpAutoGeneratedSeName, strings.TrimSuffix(dataId, ".json")),
|
||||
},
|
||||
Spec: &v1alpha3.ServiceEntry{
|
||||
Hosts: []string{"explore.DEFAULT-GROUP.public.nacos"},
|
||||
Ports: []*v1alpha3.ServicePort{
|
||||
{
|
||||
Number: 8080,
|
||||
Name: "HTTP",
|
||||
Protocol: "HTTP",
|
||||
},
|
||||
},
|
||||
Location: v1alpha3.ServiceEntry_MESH_INTERNAL,
|
||||
Resolution: v1alpha3.ServiceEntry_STATIC,
|
||||
Endpoints: []*v1alpha3.WorkloadEntry{
|
||||
{
|
||||
Address: "127.0.0.1",
|
||||
Ports: map[string]uint32{
|
||||
"HTTP": 8080,
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"protocol": "http",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
gvk.VirtualService.String(): &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.VirtualService,
|
||||
Name: fmt.Sprintf("%s-%s", provider.IstioMcpAutoGeneratedVsName, strings.TrimSuffix(dataId, ".json")),
|
||||
},
|
||||
Spec: &v1alpha3.VirtualService{
|
||||
Gateways: []string{"/" + common2.CleanHost("mcp.com"), common2.CreateConvertedName(constants.IstioIngressGatewayName, common2.CleanHost("mcp.com"))},
|
||||
Hosts: []string{"mcp.com"},
|
||||
Http: []*v1alpha3.HTTPRoute{
|
||||
{
|
||||
Name: fmt.Sprintf("%s-%s", provider.IstioMcpAutoGeneratedHttpRouteName, strings.TrimSuffix(dataId, ".json")),
|
||||
Match: []*v1alpha3.HTTPMatchRequest{
|
||||
{
|
||||
Uri: &v1alpha3.StringMatch{
|
||||
MatchType: &v1alpha3.StringMatch_Exact{
|
||||
Exact: "/mcp-servers/explore",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Uri: &v1alpha3.StringMatch{
|
||||
MatchType: &v1alpha3.StringMatch_Prefix{
|
||||
Prefix: "/mcp-servers/explore/",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Route: []*v1alpha3.HTTPRouteDestination{
|
||||
{
|
||||
Destination: &v1alpha3.Destination{
|
||||
Host: "explore.DEFAULT-GROUP.public.nacos",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sse and dns endpoint case",
|
||||
dataId: dataId,
|
||||
msc: &McpServerConfig{
|
||||
Credentials: map[string]interface{}{
|
||||
"test-server": map[string]string{"data": "value"},
|
||||
},
|
||||
ServiceInfo: &model.Service{
|
||||
Hosts: []model.Instance{
|
||||
{
|
||||
Ip: "example.com",
|
||||
Port: 8080,
|
||||
Metadata: map[string]string{"protocol": "http"},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServerSpecConfig: `{
|
||||
"name": "explore",
|
||||
"protocol": "mcp-sse",
|
||||
"description": "explore",
|
||||
"remoteServerConfig": {
|
||||
"serviceRef": {
|
||||
"namespaceId": "public",
|
||||
"groupName": "DEFAULT_GROUP",
|
||||
"serviceName": "explore"
|
||||
},
|
||||
"exportPath": ""
|
||||
},
|
||||
"enabled": true
|
||||
}`,
|
||||
ToolsSpecConfig: `{
|
||||
"tools": [
|
||||
{
|
||||
"name": "explore",
|
||||
"description": "find name from tag",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "string",
|
||||
"description": "tag"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"toolsMeta": {
|
||||
"explore": {
|
||||
"enabled": true,
|
||||
"templates": {
|
||||
"json-go-template": {
|
||||
"requestTemplate": {
|
||||
"method": "GET",
|
||||
"url": "/v0/explore",
|
||||
"argsToUrlParam": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
wantConfig: map[string]*config.Config{
|
||||
gvk.ServiceEntry.String(): &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.ServiceEntry,
|
||||
Name: fmt.Sprintf("%s-%s", provider.IstioMcpAutoGeneratedSeName, strings.TrimSuffix(dataId, ".json")),
|
||||
},
|
||||
Spec: &v1alpha3.ServiceEntry{
|
||||
Hosts: []string{"explore.DEFAULT-GROUP.public.nacos"},
|
||||
Ports: []*v1alpha3.ServicePort{
|
||||
{
|
||||
Number: 8080,
|
||||
Name: "HTTP",
|
||||
Protocol: "HTTP",
|
||||
},
|
||||
},
|
||||
Location: v1alpha3.ServiceEntry_MESH_INTERNAL,
|
||||
Resolution: v1alpha3.ServiceEntry_DNS,
|
||||
Endpoints: []*v1alpha3.WorkloadEntry{
|
||||
{
|
||||
Address: "example.com",
|
||||
Ports: map[string]uint32{
|
||||
"HTTP": 8080,
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"protocol": "http",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
gvk.VirtualService.String(): &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.VirtualService,
|
||||
Name: fmt.Sprintf("%s-%s", provider.IstioMcpAutoGeneratedVsName, strings.TrimSuffix(dataId, ".json")),
|
||||
},
|
||||
Spec: &v1alpha3.VirtualService{
|
||||
Gateways: []string{"/" + common2.CleanHost("mcp.com"), common2.CreateConvertedName(constants.IstioIngressGatewayName, common2.CleanHost("mcp.com"))},
|
||||
Hosts: []string{"mcp.com"},
|
||||
Http: []*v1alpha3.HTTPRoute{
|
||||
{
|
||||
Name: fmt.Sprintf("%s-%s", provider.IstioMcpAutoGeneratedHttpRouteName, strings.TrimSuffix(dataId, ".json")),
|
||||
Match: []*v1alpha3.HTTPMatchRequest{
|
||||
{
|
||||
Uri: &v1alpha3.StringMatch{
|
||||
MatchType: &v1alpha3.StringMatch_Exact{
|
||||
Exact: "/mcp-servers/explore",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Uri: &v1alpha3.StringMatch{
|
||||
MatchType: &v1alpha3.StringMatch_Prefix{
|
||||
Prefix: "/mcp-servers/explore/",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Route: []*v1alpha3.HTTPRouteDestination{
|
||||
{
|
||||
Destination: &v1alpha3.Destination{
|
||||
Host: "explore.DEFAULT-GROUP.public.nacos",
|
||||
},
|
||||
},
|
||||
},
|
||||
Rewrite: &v1alpha3.HTTPRewrite{
|
||||
Uri: "/",
|
||||
Authority: "example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
gvk.DestinationRule.String(): &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.DestinationRule,
|
||||
Name: fmt.Sprintf("%s-%s", provider.IstioMcpAutoGeneratedDrName, strings.TrimSuffix(dataId, ".json")),
|
||||
},
|
||||
Spec: &v1alpha3.DestinationRule{
|
||||
Host: "explore.DEFAULT-GROUP.public.nacos",
|
||||
TrafficPolicy: &v1alpha3.TrafficPolicy{
|
||||
LoadBalancer: &v1alpha3.LoadBalancerSettings{
|
||||
LbPolicy: &v1alpha3.LoadBalancerSettings_ConsistentHash{
|
||||
ConsistentHash: &v1alpha3.LoadBalancerSettings_ConsistentHashLB{
|
||||
HashKey: &v1alpha3.LoadBalancerSettings_ConsistentHashLB_UseSourceIp{
|
||||
UseSourceIp: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "https and dns case",
|
||||
dataId: dataId,
|
||||
msc: &McpServerConfig{
|
||||
Credentials: map[string]interface{}{
|
||||
"test-server": map[string]string{"data": "value"},
|
||||
},
|
||||
ServiceInfo: &model.Service{
|
||||
Hosts: []model.Instance{
|
||||
{
|
||||
Ip: "example.com",
|
||||
Port: 8080,
|
||||
Metadata: map[string]string{"protocol": "https"},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServerSpecConfig: `{
|
||||
"name": "explore",
|
||||
"protocol": "https",
|
||||
"description": "explore",
|
||||
"remoteServerConfig": {
|
||||
"serviceRef": {
|
||||
"namespaceId": "public",
|
||||
"groupName": "DEFAULT_GROUP",
|
||||
"serviceName": "explore"
|
||||
},
|
||||
"exportPath": ""
|
||||
},
|
||||
"enabled": true
|
||||
}`,
|
||||
ToolsSpecConfig: `{
|
||||
"tools": [
|
||||
{
|
||||
"name": "explore",
|
||||
"description": "find name from tag",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "string",
|
||||
"description": "tag"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"toolsMeta": {
|
||||
"explore": {
|
||||
"enabled": true,
|
||||
"templates": {
|
||||
"json-go-template": {
|
||||
"requestTemplate": {
|
||||
"method": "GET",
|
||||
"url": "/v0/explore",
|
||||
"argsToUrlParam": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
wantConfig: map[string]*config.Config{
|
||||
gvk.ServiceEntry.String(): &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.ServiceEntry,
|
||||
Name: fmt.Sprintf("%s-%s", provider.IstioMcpAutoGeneratedSeName, strings.TrimSuffix(dataId, ".json")),
|
||||
},
|
||||
Spec: &v1alpha3.ServiceEntry{
|
||||
Hosts: []string{"explore.DEFAULT-GROUP.public.nacos"},
|
||||
Ports: []*v1alpha3.ServicePort{
|
||||
{
|
||||
Number: 8080,
|
||||
Name: "HTTPS",
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
},
|
||||
Location: v1alpha3.ServiceEntry_MESH_INTERNAL,
|
||||
Resolution: v1alpha3.ServiceEntry_DNS,
|
||||
Endpoints: []*v1alpha3.WorkloadEntry{
|
||||
{
|
||||
Address: "example.com",
|
||||
Ports: map[string]uint32{
|
||||
"HTTPS": 8080,
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"protocol": "https",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
gvk.VirtualService.String(): &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.VirtualService,
|
||||
Name: fmt.Sprintf("%s-%s", provider.IstioMcpAutoGeneratedVsName, strings.TrimSuffix(dataId, ".json")),
|
||||
},
|
||||
Spec: &v1alpha3.VirtualService{
|
||||
Gateways: []string{"/" + common2.CleanHost("mcp.com"), common2.CreateConvertedName(constants.IstioIngressGatewayName, common2.CleanHost("mcp.com"))},
|
||||
Hosts: []string{"mcp.com"},
|
||||
Http: []*v1alpha3.HTTPRoute{
|
||||
{
|
||||
Name: fmt.Sprintf("%s-%s", provider.IstioMcpAutoGeneratedHttpRouteName, strings.TrimSuffix(dataId, ".json")),
|
||||
Match: []*v1alpha3.HTTPMatchRequest{
|
||||
{
|
||||
Uri: &v1alpha3.StringMatch{
|
||||
MatchType: &v1alpha3.StringMatch_Exact{
|
||||
Exact: "/mcp-servers/explore",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Uri: &v1alpha3.StringMatch{
|
||||
MatchType: &v1alpha3.StringMatch_Prefix{
|
||||
Prefix: "/mcp-servers/explore/",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Route: []*v1alpha3.HTTPRouteDestination{
|
||||
{
|
||||
Destination: &v1alpha3.Destination{
|
||||
Host: "explore.DEFAULT-GROUP.public.nacos",
|
||||
},
|
||||
},
|
||||
},
|
||||
Rewrite: &v1alpha3.HTTPRewrite{
|
||||
Authority: "example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
gvk.DestinationRule.String(): &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.DestinationRule,
|
||||
Name: fmt.Sprintf("%s-%s", provider.IstioMcpAutoGeneratedDrName, strings.TrimSuffix(dataId, ".json")),
|
||||
},
|
||||
Spec: &v1alpha3.DestinationRule{
|
||||
Host: "explore.DEFAULT-GROUP.public.nacos",
|
||||
TrafficPolicy: &v1alpha3.TrafficPolicy{
|
||||
Tls: &v1alpha3.ClientTLSSettings{
|
||||
Mode: v1alpha3.ClientTLSSettings_SIMPLE,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCase {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
localCache := testCallback(tc.msc)
|
||||
se := localCache.GetAllConfigs(gvk.ServiceEntry)[dataId]
|
||||
wantSe := tc.wantConfig[gvk.ServiceEntry.String()]
|
||||
if !reflect.DeepEqual(se, wantSe) {
|
||||
t.Errorf("se is not equal, want %v\n, got %v", wantSe, se)
|
||||
}
|
||||
|
||||
vs := localCache.GetAllConfigs(gvk.VirtualService)[dataId]
|
||||
wantVs := tc.wantConfig[gvk.VirtualService.String()]
|
||||
if !reflect.DeepEqual(vs, wantVs) {
|
||||
t.Errorf("vs is not equal, want %v\n, got %v", wantVs, vs)
|
||||
}
|
||||
|
||||
dr := localCache.GetAllConfigs(gvk.DestinationRule)[dataId]
|
||||
wantDr := tc.wantConfig[gvk.DestinationRule.String()]
|
||||
if !reflect.DeepEqual(dr, wantDr) {
|
||||
t.Errorf("dr is not equal, want %v\n, got %v", wantDr, dr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user