mirror of
https://github.com/alibaba/higress.git
synced 2026-03-01 23:20:52 +08:00
586 lines
16 KiB
Go
586 lines
16 KiB
Go
// 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)
|
|
}
|
|
})
|
|
}
|
|
}
|