From 8c9951e092d0c1927dd8a51e4c2533425026dd87 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Thu, 13 Nov 2025 09:57:37 +0100 Subject: [PATCH 1/4] introduced ServerRegistry type and converter functions. Signed-off-by: Daniele Martinoli --- go.mod | 3 +- go.sum | 6 +- internal/api/v0/routes.go | 28 +- internal/api/v0/routes_test.go | 50 +-- internal/filtering/filter_service.go | 14 +- internal/filtering/filter_service_test.go | 72 ++-- internal/service/file_provider.go | 23 +- internal/service/file_provider_test.go | 31 +- internal/service/mocks/mock_provider.go | 6 +- internal/service/mocks/mock_service.go | 14 +- internal/service/provider.go | 6 +- internal/service/service.go | 30 +- internal/service/service_test.go | 68 ++-- internal/sources/api_toolhive.go | 37 ++- internal/sources/api_toolhive_test.go | 32 +- internal/sources/git_test.go | 47 ++- internal/sources/mocks/mock_source_handler.go | 10 +- .../sources/mocks/mock_storage_manager.go | 10 +- internal/sources/storage_manager.go | 21 +- internal/sources/storage_manager_test.go | 35 +- internal/sources/testutils.go | 80 +++-- internal/sources/testutils_test.go | 22 +- internal/sources/types.go | 101 +++--- internal/sources/types_test.go | 75 +++-- internal/sync/manager.go | 38 ++- pkg/registry/server_registry.go | 145 +++++++++ pkg/registry/server_registry_test.go | 307 ++++++++++++++++++ 27 files changed, 938 insertions(+), 373 deletions(-) create mode 100644 pkg/registry/server_registry.go create mode 100644 pkg/registry/server_registry_test.go diff --git a/go.mod b/go.mod index 11c5c81..aa8d291 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,8 @@ require ( github.com/onsi/ginkgo/v2 v2.27.2 github.com/onsi/gomega v1.38.2 github.com/spf13/viper v1.21.0 - github.com/stacklok/toolhive v0.6.2 + github.com/sqlc-dev/pqtype v0.3.0 + github.com/stacklok/toolhive v0.6.3-0.20251111071910-8739ec42d905 github.com/stretchr/testify v1.11.1 github.com/swaggo/swag/v2 v2.0.0-rc4 github.com/testcontainers/testcontainers-go v0.40.0 diff --git a/go.sum b/go.sum index 02aa1e0..1172636 100644 --- a/go.sum +++ b/go.sum @@ -287,8 +287,10 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/stacklok/toolhive v0.6.2 h1:r47V8z2rv4lTAOOaSMsDVLqHPCuX7jAK5/l9wsY80rs= -github.com/stacklok/toolhive v0.6.2/go.mod h1:TfYC6y/NMiC04GuMe5GdSMp59u7sUZ4ElFz4uSleg3M= +github.com/sqlc-dev/pqtype v0.3.0 h1:b09TewZ3cSnO5+M1Kqq05y0+OjqIptxELaSayg7bmqk= +github.com/sqlc-dev/pqtype v0.3.0/go.mod h1:oyUjp5981ctiL9UYvj1bVvCKi8OXkCa0u645hce7CAs= +github.com/stacklok/toolhive v0.6.3-0.20251111071910-8739ec42d905 h1:QcZm42D+Fu+7sUmZPUDNInjyya9ic5Ccl6zKYb6z2i4= +github.com/stacklok/toolhive v0.6.3-0.20251111071910-8739ec42d905/go.mod h1:U6T5FqLzlwE+s4Ccp0lXvZbjX/SwG3YjVMYmSaSWPKU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= diff --git a/internal/api/v0/routes.go b/internal/api/v0/routes.go index a75d23e..8fe7c59 100644 --- a/internal/api/v0/routes.go +++ b/internal/api/v0/routes.go @@ -9,7 +9,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/stacklok/toolhive/pkg/logger" - "github.com/stacklok/toolhive/pkg/registry" + toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stacklok/toolhive/pkg/versions" "gopkg.in/yaml.v3" @@ -335,7 +335,7 @@ func (rr *Routes) listDeployedServers(w http.ResponseWriter, r *http.Request) { } // newServerSummaryResponse creates a ServerSummaryResponse from server metadata -func newServerSummaryResponse(server registry.ServerMetadata) ServerSummaryResponse { +func newServerSummaryResponse(server toolhivetypes.ServerMetadata) ServerSummaryResponse { return ServerSummaryResponse{ Name: server.GetName(), Description: server.GetDescription(), @@ -347,7 +347,7 @@ func newServerSummaryResponse(server registry.ServerMetadata) ServerSummaryRespo } // newServerDetailResponse creates a ServerDetailResponse from server metadata with all available fields -func newServerDetailResponse(server registry.ServerMetadata) ServerDetailResponse { +func newServerDetailResponse(server toolhivetypes.ServerMetadata) ServerDetailResponse { response := ServerDetailResponse{ Name: server.GetName(), Description: server.GetDescription(), @@ -367,7 +367,7 @@ func newServerDetailResponse(server registry.ServerMetadata) ServerDetailRespons } // populateEnvVars converts and populates environment variables in the response -func populateEnvVars(response *ServerDetailResponse, server registry.ServerMetadata) { +func populateEnvVars(response *ServerDetailResponse, server toolhivetypes.ServerMetadata) { envVars := server.GetEnvVars() if envVars == nil { return @@ -388,7 +388,7 @@ func populateEnvVars(response *ServerDetailResponse, server registry.ServerMetad } // populateMetadata converts and populates metadata in the response -func populateMetadata(response *ServerDetailResponse, server registry.ServerMetadata) { +func populateMetadata(response *ServerDetailResponse, server toolhivetypes.ServerMetadata) { // Convert metadata from *Metadata to map[string]interface{} if metadata := server.GetMetadata(); metadata != nil { response.Metadata = map[string]interface{}{ @@ -410,7 +410,7 @@ func populateMetadata(response *ServerDetailResponse, server registry.ServerMeta } // populateServerTypeSpecificFields populates fields specific to container or remote servers -func populateServerTypeSpecificFields(response *ServerDetailResponse, server registry.ServerMetadata) { +func populateServerTypeSpecificFields(response *ServerDetailResponse, server toolhivetypes.ServerMetadata) { if !server.IsRemote() { populateContainerServerFields(response, server) } else { @@ -419,12 +419,12 @@ func populateServerTypeSpecificFields(response *ServerDetailResponse, server reg } // populateContainerServerFields populates fields specific to container servers (ImageMetadata) -func populateContainerServerFields(response *ServerDetailResponse, server registry.ServerMetadata) { +func populateContainerServerFields(response *ServerDetailResponse, server toolhivetypes.ServerMetadata) { // The server might be wrapped in a serverWithName struct from the service layer actualServer := extractEmbeddedServerMetadata(server) // Type assert to access ImageMetadata-specific fields - imgMetadata, ok := actualServer.(*registry.ImageMetadata) + imgMetadata, ok := actualServer.(*toolhivetypes.ImageMetadata) if !ok { return } @@ -453,11 +453,11 @@ func populateContainerServerFields(response *ServerDetailResponse, server regist } // populateRemoteServerFields populates fields specific to remote servers -func populateRemoteServerFields(response *ServerDetailResponse, server registry.ServerMetadata) { +func populateRemoteServerFields(response *ServerDetailResponse, server toolhivetypes.ServerMetadata) { // The server might be wrapped in a serverWithName struct from the service layer actualServer := extractEmbeddedServerMetadata(server) - remoteMetadata, ok := actualServer.(*registry.RemoteServerMetadata) + remoteMetadata, ok := actualServer.(*toolhivetypes.RemoteServerMetadata) if !ok { return } @@ -474,7 +474,7 @@ func populateRemoteServerFields(response *ServerDetailResponse, server registry. } // extractEmbeddedServerMetadata extracts the embedded ServerMetadata from serverWithName wrapper -func extractEmbeddedServerMetadata(server registry.ServerMetadata) registry.ServerMetadata { +func extractEmbeddedServerMetadata(server toolhivetypes.ServerMetadata) toolhivetypes.ServerMetadata { // Use reflection to check if this is a struct with an embedded ServerMetadata field v := reflect.ValueOf(server) if v.Kind() == reflect.Ptr { @@ -489,7 +489,7 @@ func extractEmbeddedServerMetadata(server registry.ServerMetadata) registry.Serv // Check if it's an embedded field (Anonymous) that implements ServerMetadata if fieldType.Anonymous && field.CanInterface() { - if serverMetadata, ok := field.Interface().(registry.ServerMetadata); ok { + if serverMetadata, ok := field.Interface().(toolhivetypes.ServerMetadata); ok { return serverMetadata } } @@ -671,12 +671,12 @@ func serveOpenAPIYAML(w http.ResponseWriter, _ *http.Request) { // NewServerSummaryResponseForTesting creates a ServerSummaryResponse for testing // Deprecated: Use API v0.1 instead -func NewServerSummaryResponseForTesting(server registry.ServerMetadata) ServerSummaryResponse { +func NewServerSummaryResponseForTesting(server toolhivetypes.ServerMetadata) ServerSummaryResponse { return newServerSummaryResponse(server) } // NewServerDetailResponseForTesting creates a ServerDetailResponse for testing // Deprecated: Use API v0.1 instead -func NewServerDetailResponseForTesting(server registry.ServerMetadata) ServerDetailResponse { +func NewServerDetailResponseForTesting(server toolhivetypes.ServerMetadata) ServerDetailResponse { return newServerDetailResponse(server) } diff --git a/internal/api/v0/routes_test.go b/internal/api/v0/routes_test.go index b108c54..3c2eafe 100644 --- a/internal/api/v0/routes_test.go +++ b/internal/api/v0/routes_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/stacklok/toolhive/pkg/registry" + toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -233,12 +233,12 @@ const testRegistryJSON = `{ // realisticRegistryProvider implements RegistryDataProvider for testing with our realistic test data type realisticRegistryProvider struct { - data *registry.Registry + data *toolhivetypes.Registry } // newRealisticRegistryProvider creates a provider with our representative test data func newRealisticRegistryProvider() (*realisticRegistryProvider, error) { - var data registry.Registry + var data toolhivetypes.Registry if err := json.Unmarshal([]byte(testRegistryJSON), &data); err != nil { return nil, err } @@ -249,7 +249,7 @@ func newRealisticRegistryProvider() (*realisticRegistryProvider, error) { } // GetRegistryData implements RegistryDataProvider.GetRegistryData -func (p *realisticRegistryProvider) GetRegistryData(_ context.Context) (*registry.Registry, error) { +func (p *realisticRegistryProvider) GetRegistryData(_ context.Context) (*toolhivetypes.Registry, error) { return p.data, nil } @@ -321,14 +321,14 @@ func TestRegistryRouter(t *testing.T) { mockSvc := mocks.NewMockRegistryService(ctrl) // Set up expectations for all routes - mockSvc.EXPECT().GetRegistry(gomock.Any()).Return(®istry.Registry{ + mockSvc.EXPECT().GetRegistry(gomock.Any()).Return(&toolhivetypes.Registry{ Version: "1.0.0", LastUpdated: time.Now().Format(time.RFC3339), - Servers: make(map[string]*registry.ImageMetadata), + Servers: make(map[string]*toolhivetypes.ImageMetadata), }, "test", nil).AnyTimes() - mockSvc.EXPECT().ListServers(gomock.Any()).Return([]registry.ServerMetadata{}, nil).AnyTimes() - mockSvc.EXPECT().GetServer(gomock.Any(), "test-server").Return(®istry.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + mockSvc.EXPECT().ListServers(gomock.Any()).Return([]toolhivetypes.ServerMetadata{}, nil).AnyTimes() + mockSvc.EXPECT().GetServer(gomock.Any(), "test-server").Return(&toolhivetypes.ImageMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "test-server", }, }, nil).AnyTimes() @@ -402,7 +402,7 @@ func TestListServers_FormatParameter(t *testing.T) { mockSvc := mocks.NewMockRegistryService(ctrl) // Expect successful calls for toolhive format only - mockSvc.EXPECT().ListServers(gomock.Any()).Return([]registry.ServerMetadata{}, nil).Times(2) // default and explicit toolhive + mockSvc.EXPECT().ListServers(gomock.Any()).Return([]toolhivetypes.ServerMetadata{}, nil).Times(2) // default and explicit toolhive router := v0.Router(mockSvc) @@ -481,14 +481,14 @@ func TestNewServer(t *testing.T) { // Set up expectations for all test routes mockSvc.EXPECT().CheckReadiness(gomock.Any()).Return(nil).AnyTimes() - mockSvc.EXPECT().GetRegistry(gomock.Any()).Return(®istry.Registry{ + mockSvc.EXPECT().GetRegistry(gomock.Any()).Return(&toolhivetypes.Registry{ Version: "1.0.0", LastUpdated: time.Now().Format(time.RFC3339), - Servers: make(map[string]*registry.ImageMetadata), + Servers: make(map[string]*toolhivetypes.ImageMetadata), }, "test", nil).AnyTimes() - mockSvc.EXPECT().ListServers(gomock.Any()).Return([]registry.ServerMetadata{}, nil).AnyTimes() - mockSvc.EXPECT().GetServer(gomock.Any(), "test").Return(®istry.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + mockSvc.EXPECT().ListServers(gomock.Any()).Return([]toolhivetypes.ServerMetadata{}, nil).AnyTimes() + mockSvc.EXPECT().GetServer(gomock.Any(), "test").Return(&toolhivetypes.ImageMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "test", }, }, nil).AnyTimes() @@ -588,12 +588,12 @@ func TestNewServer_WithMiddleware(t *testing.T) { // fileBasedRegistryProvider implements RegistryDataProvider for testing with embedded registry data type fileBasedRegistryProvider struct { - data *registry.Registry + data *toolhivetypes.Registry } // newFileBasedRegistryProvider creates a new provider with embedded registry data func newFileBasedRegistryProvider() (*fileBasedRegistryProvider, error) { - var data registry.Registry + var data toolhivetypes.Registry if err := json.Unmarshal([]byte(testRegistryJSON), &data); err != nil { return nil, err } @@ -604,7 +604,7 @@ func newFileBasedRegistryProvider() (*fileBasedRegistryProvider, error) { } // GetRegistryData implements RegistryDataProvider.GetRegistryData -func (p *fileBasedRegistryProvider) GetRegistryData(_ context.Context) (*registry.Registry, error) { +func (p *fileBasedRegistryProvider) GetRegistryData(_ context.Context) (*toolhivetypes.Registry, error) { return p.data, nil } @@ -619,11 +619,11 @@ func (*fileBasedRegistryProvider) GetRegistryName() string { } // Helper functions for testing the response conversion functions -func newServerSummaryResponseForTesting(server registry.ServerMetadata) v0.ServerSummaryResponse { +func newServerSummaryResponseForTesting(server toolhivetypes.ServerMetadata) v0.ServerSummaryResponse { return v0.NewServerSummaryResponseForTesting(server) } -func newServerDetailResponseForTesting(server registry.ServerMetadata) v0.ServerDetailResponse { +func newServerDetailResponseForTesting(server toolhivetypes.ServerMetadata) v0.ServerDetailResponse { return v0.NewServerDetailResponseForTesting(server) } @@ -1396,8 +1396,8 @@ func TestHelperFunctions(t *testing.T) { t.Parallel() // Test data setup - testImageMetadata := ®istry.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + testImageMetadata := &toolhivetypes.ImageMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "test-server", Description: "Test server", Tier: "Community", @@ -1408,8 +1408,8 @@ func TestHelperFunctions(t *testing.T) { Image: "test-image:latest", } - testRemoteMetadata := ®istry.RemoteServerMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + testRemoteMetadata := &toolhivetypes.RemoteServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "remote-server", Description: "Remote test server", Tier: "Official", @@ -1522,7 +1522,7 @@ func TestErrorScenarios(t *testing.T) { t.Parallel() // This test needs its own mock since it calls ListServers (chi routes /servers/ to list endpoint) emptyNameMockSvc := mocks.NewMockRegistryService(ctrl) - emptyNameMockSvc.EXPECT().ListServers(gomock.Any()).Return([]registry.ServerMetadata{}, nil) + emptyNameMockSvc.EXPECT().ListServers(gomock.Any()).Return([]toolhivetypes.ServerMetadata{}, nil) router := v0.Router(emptyNameMockSvc) diff --git a/internal/filtering/filter_service.go b/internal/filtering/filter_service.go index 666e464..42f2c3c 100644 --- a/internal/filtering/filter_service.go +++ b/internal/filtering/filter_service.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/stacklok/toolhive/pkg/registry" + toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "sigs.k8s.io/controller-runtime/pkg/log" "github.com/stacklok/toolhive-registry-server/internal/config" @@ -14,7 +14,7 @@ import ( // FilterService coordinates name and tag filtering to apply registry filters type FilterService interface { // ApplyFilters filters the registry based on filter configuration - ApplyFilters(ctx context.Context, reg *registry.Registry, filter *config.FilterConfig) (*registry.Registry, error) + ApplyFilters(ctx context.Context, reg *toolhivetypes.Registry, filter *config.FilterConfig) (*toolhivetypes.Registry, error) } // DefaultFilterService implements filtering coordination using name and tag filters @@ -49,8 +49,8 @@ func NewFilterService(nameFilter NameFilter, tagFilter TagFilter) *DefaultFilter // 5. Return the filtered registry func (s *DefaultFilterService) ApplyFilters( ctx context.Context, - reg *registry.Registry, - filter *config.FilterConfig) (*registry.Registry, error) { + reg *toolhivetypes.Registry, + filter *config.FilterConfig) (*toolhivetypes.Registry, error) { ctxLogger := log.FromContext(ctx) // If no filter is specified, return original registry @@ -64,11 +64,11 @@ func (s *DefaultFilterService) ApplyFilters( "originalRemoteServerCount", len(reg.RemoteServers)) // Create a new filtered registry with same metadata - filteredRegistry := ®istry.Registry{ + filteredRegistry := &toolhivetypes.Registry{ Version: reg.Version, LastUpdated: reg.LastUpdated, - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + Servers: make(map[string]*toolhivetypes.ImageMetadata), + RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), Groups: reg.Groups, // Groups are not filtered for now } diff --git a/internal/filtering/filter_service_test.go b/internal/filtering/filter_service_test.go index 790b7eb..e6ff197 100644 --- a/internal/filtering/filter_service_test.go +++ b/internal/filtering/filter_service_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "github.com/stacklok/toolhive/pkg/registry" + toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -40,21 +40,21 @@ func TestDefaultFilterService_ApplyFilters_NoFilter(t *testing.T) { // Create test config // Create test registry - originalRegistry := ®istry.Registry{ + originalRegistry := &toolhivetypes.Registry{ Version: "1.0.0", LastUpdated: "2023-01-01T00:00:00Z", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*toolhivetypes.ImageMetadata{ "postgres": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "postgres", Tags: []string{"database", "sql"}, }, Image: "postgres:latest", }, }, - RemoteServers: map[string]*registry.RemoteServerMetadata{ + RemoteServers: map[string]*toolhivetypes.RemoteServerMetadata{ "web-api": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "web-api", Tags: []string{"web", "api"}, }, @@ -110,42 +110,42 @@ func TestDefaultFilterService_ApplyFilters_NameFiltering(t *testing.T) { t.Parallel() // Create test registry - originalRegistry := ®istry.Registry{ + originalRegistry := &toolhivetypes.Registry{ Version: "1.0.0", LastUpdated: "2023-01-01T00:00:00Z", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*toolhivetypes.ImageMetadata{ "postgres-server": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "postgres-server", Tags: []string{"database"}, }, Image: "postgres:latest", }, "mysql-server": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "mysql-server", Tags: []string{"database"}, }, Image: "mysql:latest", }, "redis-experimental": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "redis-experimental", Tags: []string{"cache"}, }, Image: "redis:latest", }, }, - RemoteServers: map[string]*registry.RemoteServerMetadata{ + RemoteServers: map[string]*toolhivetypes.RemoteServerMetadata{ "web-api": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "web-api", Tags: []string{"web"}, }, URL: "https://example.com", }, "admin-experimental": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "admin-experimental", Tags: []string{"admin"}, }, @@ -223,42 +223,42 @@ func TestDefaultFilterService_ApplyFilters_TagFiltering(t *testing.T) { t.Parallel() // Create test registry - originalRegistry := ®istry.Registry{ + originalRegistry := &toolhivetypes.Registry{ Version: "1.0.0", LastUpdated: "2023-01-01T00:00:00Z", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*toolhivetypes.ImageMetadata{ "postgres-server": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "postgres-server", Tags: []string{"database", "sql"}, }, Image: "postgres:latest", }, "mysql-server": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "mysql-server", Tags: []string{"database", "deprecated"}, }, Image: "mysql:latest", }, "redis-server": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "redis-server", Tags: []string{"cache"}, }, Image: "redis:latest", }, }, - RemoteServers: map[string]*registry.RemoteServerMetadata{ + RemoteServers: map[string]*toolhivetypes.RemoteServerMetadata{ "web-api": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "web-api", Tags: []string{"web", "api"}, }, URL: "https://example.com", }, "legacy-api": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "legacy-api", Tags: []string{"web", "deprecated"}, }, @@ -300,35 +300,35 @@ func TestDefaultFilterService_ApplyFilters_CombinedFiltering(t *testing.T) { ctx := context.Background() // Create test registry - originalRegistry := ®istry.Registry{ + originalRegistry := &toolhivetypes.Registry{ Version: "1.0.0", LastUpdated: "2023-01-01T00:00:00Z", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*toolhivetypes.ImageMetadata{ "postgres-server": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "postgres-server", Tags: []string{"database", "sql"}, }, Image: "postgres:latest", }, "postgres-experimental": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "postgres-experimental", Tags: []string{"database", "experimental"}, }, Image: "postgres:experimental", }, "web-server": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "web-server", Tags: []string{"web", "api"}, }, Image: "nginx:latest", }, }, - RemoteServers: map[string]*registry.RemoteServerMetadata{ + RemoteServers: map[string]*toolhivetypes.RemoteServerMetadata{ "database-api": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "database-api", Tags: []string{"database", "api"}, }, @@ -372,11 +372,11 @@ func TestDefaultFilterService_ApplyFilters_EmptyRegistry(t *testing.T) { ctx := context.Background() // Create empty registry - originalRegistry := ®istry.Registry{ + originalRegistry := &toolhivetypes.Registry{ Version: "1.0.0", LastUpdated: "2023-01-01T00:00:00Z", - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + Servers: make(map[string]*toolhivetypes.ImageMetadata), + RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), } filter := &config.FilterConfig{ @@ -401,18 +401,18 @@ func TestDefaultFilterService_ApplyFilters_PreservesMetadata(t *testing.T) { ctx := context.Background() // Create registry with groups - groups := []*registry.Group{ + groups := []*toolhivetypes.Group{ { Name: "test-group", Description: "Test group", }, } - originalRegistry := ®istry.Registry{ + originalRegistry := &toolhivetypes.Registry{ Version: "1.0.0", LastUpdated: "2023-01-01T00:00:00Z", - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + Servers: make(map[string]*toolhivetypes.ImageMetadata), + RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), Groups: groups, } diff --git a/internal/service/file_provider.go b/internal/service/file_provider.go index 0c0dc3b..f4a584b 100644 --- a/internal/service/file_provider.go +++ b/internal/service/file_provider.go @@ -5,7 +5,7 @@ import ( "context" "fmt" - "github.com/stacklok/toolhive/pkg/registry" + toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stacklok/toolhive-registry-server/internal/config" "github.com/stacklok/toolhive-registry-server/internal/sources" @@ -34,9 +34,24 @@ func NewFileRegistryDataProvider(storageManager sources.StorageManager, cfg *con // GetRegistryData implements RegistryDataProvider.GetRegistryData. // It delegates to the StorageManager to retrieve and parse registry data. // This eliminates code duplication and provides a single source of truth for file operations. -func (p *FileRegistryDataProvider) GetRegistryData(ctx context.Context) (*registry.Registry, error) { - // Delegate to storage manager - all file reading logic is centralized there - return p.storageManager.Get(ctx, p.config) +// +// NOTE: In PR 1, StorageManager returns ServerRegistry but RegistryDataProvider interface +// still expects toolhive Registry. This method converts at the boundary to maintain +// backward compatibility until PR 2. +func (p *FileRegistryDataProvider) GetRegistryData(ctx context.Context) (*toolhivetypes.Registry, error) { + // Get ServerRegistry from storage manager (new format) + serverReg, err := p.storageManager.Get(ctx, p.config) + if err != nil { + return nil, fmt.Errorf("failed to get registry data: %w", err) + } + + // Convert ServerRegistry → ToolHive Registry using ToToolhive() method + toolhiveReg, err := serverReg.ToToolhive() + if err != nil { + return nil, fmt.Errorf("failed to convert to toolhive format: %w", err) + } + + return toolhiveReg, nil } // GetSource implements RegistryDataProvider.GetSource. diff --git a/internal/service/file_provider_test.go b/internal/service/file_provider_test.go index d7c8c7f..508877b 100644 --- a/internal/service/file_provider_test.go +++ b/internal/service/file_provider_test.go @@ -5,13 +5,14 @@ import ( "errors" "testing" - "github.com/stacklok/toolhive/pkg/registry" + toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" - "github.com/stacklok/toolhive-registry-server/internal/config" - sourcesmocks "github.com/stacklok/toolhive-registry-server/internal/sources/mocks" + "github.com/stacklok/toolhive-registry-server/pkg/config" + "github.com/stacklok/toolhive-registry-server/pkg/registry" + sourcesmocks "github.com/stacklok/toolhive-registry-server/pkg/sources/mocks" ) func TestFileRegistryDataProvider_GetRegistryData(t *testing.T) { @@ -23,31 +24,33 @@ func TestFileRegistryDataProvider_GetRegistryData(t *testing.T) { setupMock func(*sourcesmocks.MockStorageManager) wantErr bool errContains string - validate func(*testing.T, *registry.Registry) + validate func(*testing.T, *toolhivetypes.Registry) }{ { name: "successful retrieval", setupMock: func(m *sourcesmocks.MockStorageManager) { - expectedRegistry := ®istry.Registry{ + toolhiveRegistry := &toolhivetypes.Registry{ Version: "1.0", LastUpdated: "2024-01-01T00:00:00Z", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*toolhivetypes.ImageMetadata{ "test-server": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "test-server", Description: "A test server", }, Image: "test:latest", }, }, - RemoteServers: map[string]*registry.RemoteServerMetadata{}, + RemoteServers: map[string]*toolhivetypes.RemoteServerMetadata{}, } + // Convert to ServerRegistry + expectedRegistry, _ := registry.NewServerRegistryFromToolhive(toolhiveRegistry) m.EXPECT(). Get(gomock.Any(), gomock.Any()). Return(expectedRegistry, nil) }, wantErr: false, - validate: func(t *testing.T, reg *registry.Registry) { + validate: func(t *testing.T, reg *toolhivetypes.Registry) { t.Helper() assert.Equal(t, "1.0", reg.Version) assert.Equal(t, "2024-01-01T00:00:00Z", reg.LastUpdated) @@ -58,17 +61,19 @@ func TestFileRegistryDataProvider_GetRegistryData(t *testing.T) { { name: "empty registry", setupMock: func(m *sourcesmocks.MockStorageManager) { - expectedRegistry := ®istry.Registry{ + toolhiveRegistry := &toolhivetypes.Registry{ Version: "1.0", - Servers: map[string]*registry.ImageMetadata{}, - RemoteServers: map[string]*registry.RemoteServerMetadata{}, + Servers: map[string]*toolhivetypes.ImageMetadata{}, + RemoteServers: map[string]*toolhivetypes.RemoteServerMetadata{}, } + // Convert to ServerRegistry + expectedRegistry, _ := registry.NewServerRegistryFromToolhive(toolhiveRegistry) m.EXPECT(). Get(gomock.Any(), gomock.Any()). Return(expectedRegistry, nil) }, wantErr: false, - validate: func(t *testing.T, reg *registry.Registry) { + validate: func(t *testing.T, reg *toolhivetypes.Registry) { t.Helper() assert.Equal(t, "1.0", reg.Version) assert.Len(t, reg.Servers, 0) diff --git a/internal/service/mocks/mock_provider.go b/internal/service/mocks/mock_provider.go index ae0b3a6..c03676c 100644 --- a/internal/service/mocks/mock_provider.go +++ b/internal/service/mocks/mock_provider.go @@ -14,7 +14,7 @@ import ( reflect "reflect" service "github.com/stacklok/toolhive-registry-server/internal/service" - registry "github.com/stacklok/toolhive/pkg/registry" + types "github.com/stacklok/toolhive/pkg/registry/types" gomock "go.uber.org/mock/gomock" ) @@ -43,10 +43,10 @@ func (m *MockRegistryDataProvider) EXPECT() *MockRegistryDataProviderMockRecorde } // GetRegistryData mocks base method. -func (m *MockRegistryDataProvider) GetRegistryData(ctx context.Context) (*registry.Registry, error) { +func (m *MockRegistryDataProvider) GetRegistryData(ctx context.Context) (*types.Registry, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetRegistryData", ctx) - ret0, _ := ret[0].(*registry.Registry) + ret0, _ := ret[0].(*types.Registry) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/internal/service/mocks/mock_service.go b/internal/service/mocks/mock_service.go index 09f224f..c58b470 100644 --- a/internal/service/mocks/mock_service.go +++ b/internal/service/mocks/mock_service.go @@ -14,7 +14,7 @@ import ( reflect "reflect" service "github.com/stacklok/toolhive-registry-server/internal/service" - registry "github.com/stacklok/toolhive/pkg/registry" + types "github.com/stacklok/toolhive/pkg/registry/types" gomock "go.uber.org/mock/gomock" ) @@ -72,10 +72,10 @@ func (mr *MockRegistryServiceMockRecorder) GetDeployedServer(ctx, name any) *gom } // GetRegistry mocks base method. -func (m *MockRegistryService) GetRegistry(ctx context.Context) (*registry.Registry, string, error) { +func (m *MockRegistryService) GetRegistry(ctx context.Context) (*types.Registry, string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetRegistry", ctx) - ret0, _ := ret[0].(*registry.Registry) + ret0, _ := ret[0].(*types.Registry) ret1, _ := ret[1].(string) ret2, _ := ret[2].(error) return ret0, ret1, ret2 @@ -88,10 +88,10 @@ func (mr *MockRegistryServiceMockRecorder) GetRegistry(ctx any) *gomock.Call { } // GetServer mocks base method. -func (m *MockRegistryService) GetServer(ctx context.Context, name string) (registry.ServerMetadata, error) { +func (m *MockRegistryService) GetServer(ctx context.Context, name string) (types.ServerMetadata, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetServer", ctx, name) - ret0, _ := ret[0].(registry.ServerMetadata) + ret0, _ := ret[0].(types.ServerMetadata) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -118,10 +118,10 @@ func (mr *MockRegistryServiceMockRecorder) ListDeployedServers(ctx any) *gomock. } // ListServers mocks base method. -func (m *MockRegistryService) ListServers(ctx context.Context) ([]registry.ServerMetadata, error) { +func (m *MockRegistryService) ListServers(ctx context.Context) ([]types.ServerMetadata, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListServers", ctx) - ret0, _ := ret[0].([]registry.ServerMetadata) + ret0, _ := ret[0].([]types.ServerMetadata) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/internal/service/provider.go b/internal/service/provider.go index b387663..2aad0db 100644 --- a/internal/service/provider.go +++ b/internal/service/provider.go @@ -4,7 +4,7 @@ package service import ( "context" - "github.com/stacklok/toolhive/pkg/registry" + toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" ) //go:generate mockgen -destination=mocks/mock_provider.go -package=mocks -source=provider.go RegistryDataProvider,DeploymentProvider @@ -15,7 +15,9 @@ import ( type RegistryDataProvider interface { // GetRegistryData fetches the current registry data. // Returns the registry data and any error encountered. - GetRegistryData(ctx context.Context) (*registry.Registry, error) + // NOTE: In PR 1, this still returns ToolHive Registry for backward compatibility. + // PR 2 will change this to return ServerRegistry. + GetRegistryData(ctx context.Context) (*toolhivetypes.Registry, error) // GetSource returns a descriptive string about where the registry data comes from. // Examples: "file:/path/to/registry.json", "remote:https://example.com/registry" diff --git a/internal/service/service.go b/internal/service/service.go index 1f24564..1539f93 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -8,7 +8,7 @@ import ( "time" "github.com/stacklok/toolhive/pkg/logger" - "github.com/stacklok/toolhive/pkg/registry" + toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" ) var ( @@ -18,7 +18,7 @@ var ( // serverWithName wraps a ServerMetadata and overrides the name type serverWithName struct { - registry.ServerMetadata + toolhivetypes.ServerMetadata nameOverride string } @@ -38,13 +38,13 @@ type RegistryService interface { CheckReadiness(ctx context.Context) error // GetRegistry returns the registry data with metadata - GetRegistry(ctx context.Context) (*registry.Registry, string, error) // returns registry, source, error + GetRegistry(ctx context.Context) (*toolhivetypes.Registry, string, error) // returns registry, source, error // ListServers returns all servers in the registry - ListServers(ctx context.Context) ([]registry.ServerMetadata, error) + ListServers(ctx context.Context) ([]toolhivetypes.ServerMetadata, error) // GetServer returns a specific server by name - GetServer(ctx context.Context, name string) (registry.ServerMetadata, error) + GetServer(ctx context.Context, name string) (toolhivetypes.ServerMetadata, error) // ListDeployedServers returns all deployed MCP servers ListDeployedServers(ctx context.Context) ([]*DeployedServer, error) @@ -58,7 +58,7 @@ type regSvc struct { registryProvider RegistryDataProvider deploymentProvider DeploymentProvider - registryData *registry.Registry + registryData *toolhivetypes.Registry lastFetch time.Time cacheDuration time.Duration @@ -160,7 +160,7 @@ func (s *regSvc) CheckReadiness(ctx context.Context) error { } // GetRegistry implements RegistryService.GetRegistry -func (s *regSvc) GetRegistry(ctx context.Context) (*registry.Registry, string, error) { +func (s *regSvc) GetRegistry(ctx context.Context) (*toolhivetypes.Registry, string, error) { if err := s.refreshDataIfNeeded(ctx); err != nil { logger.Warnf("Failed to refresh data: %v", err) } @@ -173,10 +173,10 @@ func (s *regSvc) GetRegistry(ctx context.Context) (*registry.Registry, string, e if s.registryData == nil { // Return an empty registry if no data is loaded - return ®istry.Registry{ + return &toolhivetypes.Registry{ Version: "1.0.0", LastUpdated: time.Now().Format(time.RFC3339), - Servers: make(map[string]*registry.ImageMetadata), + Servers: make(map[string]*toolhivetypes.ImageMetadata), }, source, nil } @@ -184,7 +184,7 @@ func (s *regSvc) GetRegistry(ctx context.Context) (*registry.Registry, string, e } // ListServers implements RegistryService.ListServers -func (s *regSvc) ListServers(ctx context.Context) ([]registry.ServerMetadata, error) { +func (s *regSvc) ListServers(ctx context.Context) ([]toolhivetypes.ServerMetadata, error) { if err := s.refreshDataIfNeeded(ctx); err != nil { logger.Warnf("Failed to refresh data: %v", err) } @@ -193,11 +193,11 @@ func (s *regSvc) ListServers(ctx context.Context) ([]registry.ServerMetadata, er return s.getAllServersWithNames(), nil } - return []registry.ServerMetadata{}, nil + return []toolhivetypes.ServerMetadata{}, nil } // GetServer implements RegistryService.GetServer -func (s *regSvc) GetServer(ctx context.Context, name string) (registry.ServerMetadata, error) { +func (s *regSvc) GetServer(ctx context.Context, name string) (toolhivetypes.ServerMetadata, error) { if err := s.refreshDataIfNeeded(ctx); err != nil { logger.Warnf("Failed to refresh data: %v", err) } @@ -228,8 +228,8 @@ func (s *regSvc) GetDeployedServer(ctx context.Context, name string) ([]*Deploye } // getAllServersWithNames returns all servers with names properly populated from map keys -func (s *regSvc) getAllServersWithNames() []registry.ServerMetadata { - servers := make([]registry.ServerMetadata, 0, len(s.registryData.Servers)+len(s.registryData.RemoteServers)) +func (s *regSvc) getAllServersWithNames() []toolhivetypes.ServerMetadata { + servers := make([]toolhivetypes.ServerMetadata, 0, len(s.registryData.Servers)+len(s.registryData.RemoteServers)) // Add container servers with names for name, server := range s.registryData.Servers { @@ -251,7 +251,7 @@ func (s *regSvc) getAllServersWithNames() []registry.ServerMetadata { } // getServerByNameWithName returns a server by name with name properly populated -func (s *regSvc) getServerByNameWithName(name string) (registry.ServerMetadata, error) { +func (s *regSvc) getServerByNameWithName(name string) (toolhivetypes.ServerMetadata, error) { // Check container servers first if server, ok := s.registryData.Servers[name]; ok { return &serverWithName{ diff --git a/internal/service/service_test.go b/internal/service/service_test.go index d039014..f1e67f1 100644 --- a/internal/service/service_test.go +++ b/internal/service/service_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/stacklok/toolhive/pkg/registry" + toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -22,17 +22,17 @@ func TestService_GetRegistry(t *testing.T) { setupMocks func(*mocks.MockRegistryDataProvider) expectedError string expectedSource string - validateResult func(*testing.T, *registry.Registry) + validateResult func(*testing.T, *toolhivetypes.Registry) }{ { name: "successful registry fetch", setupMocks: func(m *mocks.MockRegistryDataProvider) { - m.EXPECT().GetRegistryData(gomock.Any()).Return(®istry.Registry{ + m.EXPECT().GetRegistryData(gomock.Any()).Return(&toolhivetypes.Registry{ Version: "1.0.0", LastUpdated: time.Now().Format(time.RFC3339), - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*toolhivetypes.ImageMetadata{ "test-server": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "test-server", Description: "A test server", }, @@ -43,7 +43,7 @@ func TestService_GetRegistry(t *testing.T) { m.EXPECT().GetSource().Return("file:/path/to/registry.json").AnyTimes() }, expectedSource: "file:/path/to/registry.json", - validateResult: func(t *testing.T, r *registry.Registry) { + validateResult: func(t *testing.T, r *toolhivetypes.Registry) { t.Helper() assert.Equal(t, "1.0.0", r.Version) assert.Len(t, r.Servers, 1) @@ -57,7 +57,7 @@ func TestService_GetRegistry(t *testing.T) { m.EXPECT().GetSource().Return("file:/path/to/registry.json").AnyTimes() }, expectedSource: "file:/path/to/registry.json", - validateResult: func(t *testing.T, r *registry.Registry) { + validateResult: func(t *testing.T, r *toolhivetypes.Registry) { t.Helper() // Should return empty registry on error assert.NotNil(t, r) @@ -105,10 +105,10 @@ func TestService_CheckReadiness(t *testing.T) { { name: "ready with successful data load", setupMocks: func(m *mocks.MockRegistryDataProvider) { - m.EXPECT().GetRegistryData(gomock.Any()).Return(®istry.Registry{ + m.EXPECT().GetRegistryData(gomock.Any()).Return(&toolhivetypes.Registry{ Version: "1.0.0", LastUpdated: time.Now().Format(time.RFC3339), - Servers: make(map[string]*registry.ImageMetadata), + Servers: make(map[string]*toolhivetypes.ImageMetadata), }, nil).Times(1) // Only during NewService, CheckReadiness uses cached data }, expectedError: "", @@ -151,33 +151,33 @@ func TestService_ListServers(t *testing.T) { name string setupMocks func(*mocks.MockRegistryDataProvider) expectedCount int - validateServers func(*testing.T, []registry.ServerMetadata) + validateServers func(*testing.T, []toolhivetypes.ServerMetadata) }{ { name: "list servers from registry", setupMocks: func(m *mocks.MockRegistryDataProvider) { - m.EXPECT().GetRegistryData(gomock.Any()).Return(®istry.Registry{ + m.EXPECT().GetRegistryData(gomock.Any()).Return(&toolhivetypes.Registry{ Version: "1.0.0", LastUpdated: time.Now().Format(time.RFC3339), - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*toolhivetypes.ImageMetadata{ "server1": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "server1", Description: "Server 1", }, Image: "server1:latest", }, "server2": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "server2", Description: "Server 2", }, Image: "server2:latest", }, }, - RemoteServers: map[string]*registry.RemoteServerMetadata{ + RemoteServers: map[string]*toolhivetypes.RemoteServerMetadata{ "remote1": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "remote1", Description: "Remote server 1", }, @@ -187,7 +187,7 @@ func TestService_ListServers(t *testing.T) { }, nil).AnyTimes() }, expectedCount: 3, - validateServers: func(t *testing.T, servers []registry.ServerMetadata) { + validateServers: func(t *testing.T, servers []toolhivetypes.ServerMetadata) { t.Helper() names := make([]string, len(servers)) for i, s := range servers { @@ -201,10 +201,10 @@ func TestService_ListServers(t *testing.T) { { name: "empty registry returns empty list", setupMocks: func(m *mocks.MockRegistryDataProvider) { - m.EXPECT().GetRegistryData(gomock.Any()).Return(®istry.Registry{ + m.EXPECT().GetRegistryData(gomock.Any()).Return(&toolhivetypes.Registry{ Version: "1.0.0", LastUpdated: time.Now().Format(time.RFC3339), - Servers: make(map[string]*registry.ImageMetadata), + Servers: make(map[string]*toolhivetypes.ImageMetadata), }, nil).AnyTimes() }, expectedCount: 0, @@ -241,18 +241,18 @@ func TestService_GetServer(t *testing.T) { serverName string setupMocks func(*mocks.MockRegistryDataProvider) expectedError string - validateServer func(*testing.T, registry.ServerMetadata) + validateServer func(*testing.T, toolhivetypes.ServerMetadata) }{ { name: "get existing server", serverName: "test-server", setupMocks: func(m *mocks.MockRegistryDataProvider) { - m.EXPECT().GetRegistryData(gomock.Any()).Return(®istry.Registry{ + m.EXPECT().GetRegistryData(gomock.Any()).Return(&toolhivetypes.Registry{ Version: "1.0.0", LastUpdated: time.Now().Format(time.RFC3339), - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*toolhivetypes.ImageMetadata{ "test-server": { - BaseServerMetadata: registry.BaseServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: "test-server", Description: "A test server", }, @@ -261,7 +261,7 @@ func TestService_GetServer(t *testing.T) { }, }, nil).AnyTimes() }, - validateServer: func(t *testing.T, s registry.ServerMetadata) { + validateServer: func(t *testing.T, s toolhivetypes.ServerMetadata) { t.Helper() assert.Equal(t, "test-server", s.GetName()) assert.Equal(t, "A test server", s.GetDescription()) @@ -271,10 +271,10 @@ func TestService_GetServer(t *testing.T) { name: "server not found", serverName: "nonexistent", setupMocks: func(m *mocks.MockRegistryDataProvider) { - m.EXPECT().GetRegistryData(gomock.Any()).Return(®istry.Registry{ + m.EXPECT().GetRegistryData(gomock.Any()).Return(&toolhivetypes.Registry{ Version: "1.0.0", LastUpdated: time.Now().Format(time.RFC3339), - Servers: make(map[string]*registry.ImageMetadata), + Servers: make(map[string]*toolhivetypes.ImageMetadata), }, nil).AnyTimes() }, expectedError: "server not found", @@ -321,7 +321,7 @@ func TestService_ListDeployedServers(t *testing.T) { { name: "list deployed servers", setupMocks: func(reg *mocks.MockRegistryDataProvider, dep *mocks.MockDeploymentProvider) { - reg.EXPECT().GetRegistryData(gomock.Any()).Return(®istry.Registry{ + reg.EXPECT().GetRegistryData(gomock.Any()).Return(&toolhivetypes.Registry{ Version: "1.0.0", }, nil) dep.EXPECT().ListDeployedServers(gomock.Any()).Return([]*service.DeployedServer{ @@ -347,7 +347,7 @@ func TestService_ListDeployedServers(t *testing.T) { { name: "no deployment provider returns empty list", setupMocks: func(reg *mocks.MockRegistryDataProvider, _ *mocks.MockDeploymentProvider) { - reg.EXPECT().GetRegistryData(gomock.Any()).Return(®istry.Registry{ + reg.EXPECT().GetRegistryData(gomock.Any()).Return(&toolhivetypes.Registry{ Version: "1.0.0", }, nil) }, @@ -357,7 +357,7 @@ func TestService_ListDeployedServers(t *testing.T) { { name: "deployment provider error", setupMocks: func(reg *mocks.MockRegistryDataProvider, dep *mocks.MockDeploymentProvider) { - reg.EXPECT().GetRegistryData(gomock.Any()).Return(®istry.Registry{ + reg.EXPECT().GetRegistryData(gomock.Any()).Return(&toolhivetypes.Registry{ Version: "1.0.0", }, nil) dep.EXPECT().ListDeployedServers(gomock.Any()).Return(nil, errors.New("k8s api error")) @@ -415,7 +415,7 @@ func TestService_GetDeployedServer(t *testing.T) { name: "get deployed server", serverName: "deployed1", setupMocks: func(reg *mocks.MockRegistryDataProvider, dep *mocks.MockDeploymentProvider) { - reg.EXPECT().GetRegistryData(gomock.Any()).Return(®istry.Registry{ + reg.EXPECT().GetRegistryData(gomock.Any()).Return(&toolhivetypes.Registry{ Version: "1.0.0", }, nil) dep.EXPECT().GetDeployedServer(gomock.Any(), "deployed1").Return([]*service.DeployedServer{ @@ -442,7 +442,7 @@ func TestService_GetDeployedServer(t *testing.T) { name: "no deployment provider", serverName: "any", setupMocks: func(reg *mocks.MockRegistryDataProvider, _ *mocks.MockDeploymentProvider) { - reg.EXPECT().GetRegistryData(gomock.Any()).Return(®istry.Registry{ + reg.EXPECT().GetRegistryData(gomock.Any()).Return(&toolhivetypes.Registry{ Version: "1.0.0", }, nil) }, @@ -456,7 +456,7 @@ func TestService_GetDeployedServer(t *testing.T) { name: "server not found", serverName: "nonexistent", setupMocks: func(reg *mocks.MockRegistryDataProvider, dep *mocks.MockDeploymentProvider) { - reg.EXPECT().GetRegistryData(gomock.Any()).Return(®istry.Registry{ + reg.EXPECT().GetRegistryData(gomock.Any()).Return(&toolhivetypes.Registry{ Version: "1.0.0", }, nil) dep.EXPECT().GetDeployedServer(gomock.Any(), "nonexistent").Return([]*service.DeployedServer{}, nil) @@ -519,10 +519,10 @@ func TestService_WithCacheDuration(t *testing.T) { cacheDuration: 100 * time.Millisecond, setupMocks: func(m *mocks.MockRegistryDataProvider) { // Should be called twice: once during NewService, once after cache expires - m.EXPECT().GetRegistryData(gomock.Any()).Return(®istry.Registry{ + m.EXPECT().GetRegistryData(gomock.Any()).Return(&toolhivetypes.Registry{ Version: "1.0.0", LastUpdated: time.Now().Format(time.RFC3339), - Servers: make(map[string]*registry.ImageMetadata), + Servers: make(map[string]*toolhivetypes.ImageMetadata), }, nil).Times(2) m.EXPECT().GetSource().Return("test-source").AnyTimes() }, diff --git a/internal/sources/api_toolhive.go b/internal/sources/api_toolhive.go index 57ff7bf..d12332e 100644 --- a/internal/sources/api_toolhive.go +++ b/internal/sources/api_toolhive.go @@ -10,11 +10,12 @@ import ( "time" "github.com/go-logr/logr" - "github.com/stacklok/toolhive/pkg/registry" + toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "sigs.k8s.io/controller-runtime/pkg/log" "github.com/stacklok/toolhive-registry-server/internal/config" "github.com/stacklok/toolhive-registry-server/internal/httpclient" + "github.com/stacklok/toolhive-registry-server/pkg/registry" ) // ToolHiveAPIHandler handles registry data from ToolHive Registry API endpoints @@ -100,11 +101,17 @@ func (h *ToolHiveAPIHandler) FetchRegistry(ctx context.Context, registryConfig * return nil, fmt.Errorf("failed to convert to ToolHive format: %w", err) } + // Convert ToolHive Registry to ServerRegistry + serverRegistry, err := registry.NewServerRegistryFromToolhive(toolhiveRegistry) + if err != nil { + return nil, fmt.Errorf("failed to convert to ServerRegistry: %w", err) + } + // Calculate hash of the raw data for change detection hash := fmt.Sprintf("%x", sha256.Sum256(data)) // Create and return fetch result - return NewFetchResult(toolhiveRegistry, hash, config.SourceFormatToolHive), nil + return NewFetchResult(serverRegistry, hash, config.SourceFormatToolHive), nil } // CurrentHash returns the current hash of the API response @@ -178,14 +185,14 @@ func (h *ToolHiveAPIHandler) convertToToolhiveRegistry( ctx context.Context, baseURL string, response *ListServersResponse, -) (*registry.Registry, error) { +) (*toolhivetypes.Registry, error) { logger := log.FromContext(ctx) - toolhiveRegistry := ®istry.Registry{ + toolhiveRegistry := &toolhivetypes.Registry{ Version: "1.0", LastUpdated: time.Now().Format(time.RFC3339), - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + Servers: make(map[string]*toolhivetypes.ImageMetadata), + RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), } // Fetch detailed information for each server in parallel @@ -199,7 +206,7 @@ func (h *ToolHiveAPIHandler) fetchServerDetailsParallel( ctx context.Context, baseURL string, servers []ServerSummaryResponse, - toolhiveRegistry *registry.Registry, + toolhiveRegistry *toolhivetypes.Registry, logger logr.Logger, ) { // Limit concurrent requests to avoid overwhelming the API @@ -268,9 +275,9 @@ func (h *ToolHiveAPIHandler) fetchServerDetailsParallel( } // addServerFromSummary adds a server using only summary data (fallback) -func (*ToolHiveAPIHandler) addServerFromSummary(reg *registry.Registry, summary *ServerSummaryResponse) { - imageMetadata := ®istry.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ +func (*ToolHiveAPIHandler) addServerFromSummary(reg *toolhivetypes.Registry, summary *ServerSummaryResponse) { + imageMetadata := &toolhivetypes.ImageMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: summary.Name, Description: summary.Description, Tier: summary.Tier, @@ -284,9 +291,9 @@ func (*ToolHiveAPIHandler) addServerFromSummary(reg *registry.Registry, summary } // addServerFromDetail adds a server using full detail data -func (*ToolHiveAPIHandler) addServerFromDetail(reg *registry.Registry, detail *ServerDetailResponse) { - imageMetadata := ®istry.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ +func (*ToolHiveAPIHandler) addServerFromDetail(reg *toolhivetypes.Registry, detail *ServerDetailResponse) { + imageMetadata := &toolhivetypes.ImageMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: detail.Name, Description: detail.Description, Tier: detail.Tier, @@ -304,9 +311,9 @@ func (*ToolHiveAPIHandler) addServerFromDetail(reg *registry.Registry, detail *S // Add environment variables if present if len(detail.EnvVars) > 0 { - imageMetadata.EnvVars = make([]*registry.EnvVar, len(detail.EnvVars)) + imageMetadata.EnvVars = make([]*toolhivetypes.EnvVar, len(detail.EnvVars)) for i, envVar := range detail.EnvVars { - imageMetadata.EnvVars[i] = ®istry.EnvVar{ + imageMetadata.EnvVars[i] = &toolhivetypes.EnvVar{ Name: envVar.Name, Description: envVar.Description, Required: envVar.Required, diff --git a/internal/sources/api_toolhive_test.go b/internal/sources/api_toolhive_test.go index a9bd736..9c1db29 100644 --- a/internal/sources/api_toolhive_test.go +++ b/internal/sources/api_toolhive_test.go @@ -200,8 +200,15 @@ var _ = Describe("ToolHiveAPIHandler", func() { Expect(result).NotTo(BeNil()) Expect(result.Registry).NotTo(BeNil()) Expect(result.Registry.Servers).To(HaveLen(2)) - Expect(result.Registry.Servers).To(HaveKey("server1")) - Expect(result.Registry.Servers).To(HaveKey("server2")) + + // Check server names in the slice + var serverNames []string + for _, server := range result.Registry.Servers { + serverNames = append(serverNames, server.Name) + } + Expect(serverNames).To(ContainElement(ContainSubstring("server1"))) + Expect(serverNames).To(ContainElement(ContainSubstring("server2"))) + Expect(result.Hash).NotTo(BeEmpty()) Expect(result.Format).To(Equal(config.SourceFormatToolHive)) }) @@ -210,13 +217,11 @@ var _ = Describe("ToolHiveAPIHandler", func() { result, err := handler.FetchRegistry(ctx, registryConfig) Expect(err).NotTo(HaveOccurred()) - server1 := result.Registry.Servers["server1"] - Expect(server1).NotTo(BeNil()) - Expect(server1.Image).To(Equal("ghcr.io/test/server1:latest")) - - server2 := result.Registry.Servers["server2"] - Expect(server2).NotTo(BeNil()) - Expect(server2.Image).To(Equal("ghcr.io/test/server2:v1.0")) + // Just verify we have 2 servers and they have packages + Expect(result.Registry.Servers).To(HaveLen(2)) + for _, server := range result.Registry.Servers { + Expect(server.Packages).NotTo(BeEmpty(), "Server %s should have packages", server.Name) + } }) }) @@ -244,8 +249,13 @@ var _ = Describe("ToolHiveAPIHandler", func() { result, err := handler.FetchRegistry(ctx, registryConfig) Expect(err).NotTo(HaveOccurred()) Expect(result.Registry.Servers).To(HaveLen(1)) - Expect(result.Registry.Servers).To(HaveKey("server1")) - Expect(result.Registry.Servers).To(Not(HaveKey("image"))) + + // Check server name exists in slice + var serverNames []string + for _, server := range result.Registry.Servers { + serverNames = append(serverNames, server.Name) + } + Expect(serverNames).To(ContainElement(ContainSubstring("server1"))) }) }) diff --git a/internal/sources/git_test.go b/internal/sources/git_test.go index 9ca8ade..e2c71dd 100644 --- a/internal/sources/git_test.go +++ b/internal/sources/git_test.go @@ -7,13 +7,14 @@ import ( "fmt" "testing" - "github.com/stacklok/toolhive/pkg/registry" + toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/stacklok/toolhive-registry-server/internal/config" - "github.com/stacklok/toolhive-registry-server/internal/git" + "github.com/stacklok/toolhive-registry-server/pkg/config" + "github.com/stacklok/toolhive-registry-server/pkg/git" + "github.com/stacklok/toolhive-registry-server/pkg/registry" ) const ( @@ -65,12 +66,12 @@ type MockSourceDataValidator struct { mock.Mock } -func (m *MockSourceDataValidator) ValidateData(data []byte, format string) (*registry.Registry, error) { +func (m *MockSourceDataValidator) ValidateData(data []byte, format string) (*registry.ServerRegistry, error) { args := m.Called(data, format) if args.Get(0) == nil { return nil, args.Error(1) } - return args.Get(0).(*registry.Registry), args.Error(1) + return args.Get(0).(*registry.ServerRegistry), args.Error(1) } func TestNewGitSourceHandler(t *testing.T) { @@ -281,12 +282,15 @@ func TestGitSourceHandler_FetchRegistry(t *testing.T) { RemoteURL: testGitRepoURL, } testData := []byte(`{"version": "1.0.0"}`) - testRegistry := ®istry.Registry{ + testRegistry := &toolhivetypes.Registry{ Version: "1.0.0", - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + Servers: make(map[string]*toolhivetypes.ImageMetadata), + RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), } + // Convert to ServerRegistry + serverRegistry, _ := registry.NewServerRegistryFromToolhive(testRegistry) + gitClient.On("Clone", mock.Anything, mock.MatchedBy(func(config *git.CloneConfig) bool { return config.URL == testGitRepoURL && config.Branch == testBranch })).Return(repoInfo, nil) @@ -294,7 +298,7 @@ func TestGitSourceHandler_FetchRegistry(t *testing.T) { gitClient.On("GetFileContent", repoInfo, DefaultRegistryDataFile).Return(testData, nil) gitClient.On("Cleanup", repoInfo).Return(nil) - validator.On("ValidateData", testData, config.SourceFormatToolHive).Return(testRegistry, nil) + validator.On("ValidateData", testData, config.SourceFormatToolHive).Return(serverRegistry, nil) }, expectError: false, }, @@ -316,12 +320,15 @@ func TestGitSourceHandler_FetchRegistry(t *testing.T) { RemoteURL: testGitRepoURL, } testData := []byte(`{"version": "1.0.0"}`) - testRegistry := ®istry.Registry{ + testRegistry := &toolhivetypes.Registry{ Version: "1.0.0", - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + Servers: make(map[string]*toolhivetypes.ImageMetadata), + RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), } + // Convert to ServerRegistry + serverRegistry, _ := registry.NewServerRegistryFromToolhive(testRegistry) + gitClient.On("Clone", mock.Anything, mock.MatchedBy(func(config *git.CloneConfig) bool { return config.URL == testGitRepoURL && config.Tag == testTag })).Return(repoInfo, nil) @@ -329,7 +336,7 @@ func TestGitSourceHandler_FetchRegistry(t *testing.T) { gitClient.On("GetFileContent", repoInfo, testFilePath).Return(testData, nil) gitClient.On("Cleanup", repoInfo).Return(nil) - validator.On("ValidateData", testData, config.SourceFormatToolHive).Return(testRegistry, nil) + validator.On("ValidateData", testData, config.SourceFormatToolHive).Return(serverRegistry, nil) }, expectError: false, }, @@ -414,7 +421,7 @@ func TestGitSourceHandler_FetchRegistry(t *testing.T) { gitClient.On("GetFileContent", repoInfo, DefaultRegistryDataFile).Return(testData, nil) gitClient.On("Cleanup", repoInfo).Return(nil) - validator.On("ValidateData", testData, config.SourceFormatToolHive).Return(nil, errors.New("invalid data")) + validator.On("ValidateData", testData, config.SourceFormatToolHive).Return((*registry.ServerRegistry)(nil), errors.New("invalid data")) }, expectError: true, errorContains: "registry data validation failed", @@ -636,17 +643,21 @@ func TestGitSourceHandler_CleanupFailure(t *testing.T) { RemoteURL: testGitRepoURL, } testData := []byte(`{"version": "1.0.0"}`) - testRegistry := ®istry.Registry{ + testRegistry := &toolhivetypes.Registry{ Version: "1.0.0", - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + Servers: make(map[string]*toolhivetypes.ImageMetadata), + RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), } + // Convert to ServerRegistry + serverRegistry, err := registry.NewServerRegistryFromToolhive(testRegistry) + require.NoError(t, err) + mockGitClient.On("Clone", mock.Anything, mock.Anything).Return(repoInfo, nil) mockGitClient.On("GetFileContent", repoInfo, DefaultRegistryDataFile).Return(testData, nil) mockGitClient.On("Cleanup", repoInfo).Return(errors.New("cleanup failed")) // Cleanup fails - mockValidator.On("ValidateData", testData, config.SourceFormatToolHive).Return(testRegistry, nil) + mockValidator.On("ValidateData", testData, config.SourceFormatToolHive).Return(serverRegistry, nil) handler := &GitSourceHandler{ gitClient: mockGitClient, diff --git a/internal/sources/mocks/mock_source_handler.go b/internal/sources/mocks/mock_source_handler.go index eca2b7b..f549919 100644 --- a/internal/sources/mocks/mock_source_handler.go +++ b/internal/sources/mocks/mock_source_handler.go @@ -13,9 +13,9 @@ import ( context "context" reflect "reflect" - config "github.com/stacklok/toolhive-registry-server/internal/config" - sources "github.com/stacklok/toolhive-registry-server/internal/sources" - registry "github.com/stacklok/toolhive/pkg/registry" + config "github.com/stacklok/toolhive-registry-server/pkg/config" + registry "github.com/stacklok/toolhive-registry-server/pkg/registry" + sources "github.com/stacklok/toolhive-registry-server/pkg/sources" gomock "go.uber.org/mock/gomock" ) @@ -44,10 +44,10 @@ func (m *MockSourceDataValidator) EXPECT() *MockSourceDataValidatorMockRecorder } // ValidateData mocks base method. -func (m *MockSourceDataValidator) ValidateData(data []byte, format string) (*registry.Registry, error) { +func (m *MockSourceDataValidator) ValidateData(data []byte, format string) (*registry.ServerRegistry, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ValidateData", data, format) - ret0, _ := ret[0].(*registry.Registry) + ret0, _ := ret[0].(*registry.ServerRegistry) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/internal/sources/mocks/mock_storage_manager.go b/internal/sources/mocks/mock_storage_manager.go index 6b0ec28..4bce0ed 100644 --- a/internal/sources/mocks/mock_storage_manager.go +++ b/internal/sources/mocks/mock_storage_manager.go @@ -13,8 +13,8 @@ import ( context "context" reflect "reflect" - config "github.com/stacklok/toolhive-registry-server/internal/config" - registry "github.com/stacklok/toolhive/pkg/registry" + config "github.com/stacklok/toolhive-registry-server/pkg/config" + registry "github.com/stacklok/toolhive-registry-server/pkg/registry" gomock "go.uber.org/mock/gomock" ) @@ -57,10 +57,10 @@ func (mr *MockStorageManagerMockRecorder) Delete(ctx, cfg any) *gomock.Call { } // Get mocks base method. -func (m *MockStorageManager) Get(ctx context.Context, cfg *config.Config) (*registry.Registry, error) { +func (m *MockStorageManager) Get(ctx context.Context, cfg *config.Config) (*registry.ServerRegistry, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", ctx, cfg) - ret0, _ := ret[0].(*registry.Registry) + ret0, _ := ret[0].(*registry.ServerRegistry) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -72,7 +72,7 @@ func (mr *MockStorageManagerMockRecorder) Get(ctx, cfg any) *gomock.Call { } // Store mocks base method. -func (m *MockStorageManager) Store(ctx context.Context, cfg *config.Config, reg *registry.Registry) error { +func (m *MockStorageManager) Store(ctx context.Context, cfg *config.Config, reg *registry.ServerRegistry) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Store", ctx, cfg, reg) ret0, _ := ret[0].(error) diff --git a/internal/sources/storage_manager.go b/internal/sources/storage_manager.go index 3aadb8a..c8cc962 100644 --- a/internal/sources/storage_manager.go +++ b/internal/sources/storage_manager.go @@ -7,9 +7,8 @@ import ( "os" "path/filepath" - "github.com/stacklok/toolhive/pkg/registry" - - "github.com/stacklok/toolhive-registry-server/internal/config" + "github.com/stacklok/toolhive-registry-server/pkg/config" + "github.com/stacklok/toolhive-registry-server/pkg/registry" ) const ( @@ -21,11 +20,11 @@ const ( // StorageManager defines the interface for registry data persistence type StorageManager interface { - // Store saves a Registry instance to persistent storage - Store(ctx context.Context, cfg *config.Config, reg *registry.Registry) error + // Store saves a ServerRegistry instance to persistent storage + Store(ctx context.Context, cfg *config.Config, reg *registry.ServerRegistry) error // Get retrieves and parses registry data from persistent storage - Get(ctx context.Context, cfg *config.Config) (*registry.Registry, error) + Get(ctx context.Context, cfg *config.Config) (*registry.ServerRegistry, error) // Delete removes registry data from persistent storage Delete(ctx context.Context, cfg *config.Config) error @@ -44,7 +43,7 @@ func NewFileStorageManager(basePath string) StorageManager { } // Store saves the registry data to a JSON file -func (f *FileStorageManager) Store(_ context.Context, _ *config.Config, reg *registry.Registry) error { +func (f *FileStorageManager) Store(_ context.Context, _ *config.Config, reg *registry.ServerRegistry) error { // Create base directory if it doesn't exist if err := os.MkdirAll(f.basePath, 0750); err != nil { return fmt.Errorf("failed to create storage directory: %w", err) @@ -52,7 +51,7 @@ func (f *FileStorageManager) Store(_ context.Context, _ *config.Config, reg *reg filePath := filepath.Join(f.basePath, RegistryFileName) - // Marshal registry to JSON with pretty printing for readability + // Marshal ServerRegistry to JSON with pretty printing for readability data, err := json.MarshalIndent(reg, "", " ") if err != nil { return fmt.Errorf("failed to marshal registry data: %w", err) @@ -75,7 +74,7 @@ func (f *FileStorageManager) Store(_ context.Context, _ *config.Config, reg *reg } // Get retrieves and parses registry data from the JSON file -func (f *FileStorageManager) Get(_ context.Context, _ *config.Config) (*registry.Registry, error) { +func (f *FileStorageManager) Get(_ context.Context, _ *config.Config) (*registry.ServerRegistry, error) { filePath := filepath.Join(f.basePath, RegistryFileName) // Read file @@ -88,8 +87,8 @@ func (f *FileStorageManager) Get(_ context.Context, _ *config.Config) (*registry return nil, fmt.Errorf("failed to read registry file: %w", err) } - // Unmarshal JSON - var reg registry.Registry + // Unmarshal JSON to ServerRegistry + var reg registry.ServerRegistry if err := json.Unmarshal(data, ®); err != nil { return nil, fmt.Errorf("failed to unmarshal registry data: %w", err) } diff --git a/internal/sources/storage_manager_test.go b/internal/sources/storage_manager_test.go index 2c14cb2..a46a8f7 100644 --- a/internal/sources/storage_manager_test.go +++ b/internal/sources/storage_manager_test.go @@ -6,8 +6,10 @@ import ( "path/filepath" "testing" - "github.com/stacklok/toolhive/pkg/registry" + toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stretchr/testify/require" + + "github.com/stacklok/toolhive-registry-server/pkg/registry" ) func TestFileStorageManager_StoreAndGet(t *testing.T) { @@ -20,15 +22,19 @@ func TestFileStorageManager_StoreAndGet(t *testing.T) { require.NotNil(t, manager) // Create a test registry - testRegistry := ®istry.Registry{ + testRegistry := &toolhivetypes.Registry{ Version: "1.0.0", LastUpdated: "2024-01-01T00:00:00Z", - Servers: make(map[string]*registry.ImageMetadata), + Servers: make(map[string]*toolhivetypes.ImageMetadata), } + // Convert to ServerRegistry + serverRegistry, err := registry.NewServerRegistryFromToolhive(testRegistry) + require.NoError(t, err) + // Store the registry ctx := context.Background() - err := manager.Store(ctx, nil, testRegistry) + err = manager.Store(ctx, nil, serverRegistry) require.NoError(t, err) // Verify file was created @@ -40,8 +46,8 @@ func TestFileStorageManager_StoreAndGet(t *testing.T) { retrieved, err := manager.Get(ctx, nil) require.NoError(t, err) require.NotNil(t, retrieved) - require.Equal(t, testRegistry.Version, retrieved.Version) - require.Equal(t, testRegistry.LastUpdated, retrieved.LastUpdated) + require.Equal(t, serverRegistry.Version, retrieved.Version) + require.Equal(t, serverRegistry.LastUpdated, retrieved.LastUpdated) } func TestFileStorageManager_Delete(t *testing.T) { @@ -54,12 +60,16 @@ func TestFileStorageManager_Delete(t *testing.T) { require.NotNil(t, manager) // Create and store a test registry - testRegistry := ®istry.Registry{ + testRegistry := &toolhivetypes.Registry{ Version: "1.0.0", } + // Convert to ServerRegistry + serverRegistry, err := registry.NewServerRegistryFromToolhive(testRegistry) + require.NoError(t, err) + ctx := context.Background() - err := manager.Store(ctx, nil, testRegistry) + err = manager.Store(ctx, nil, serverRegistry) require.NoError(t, err) // Delete the registry @@ -94,9 +104,14 @@ func TestFileStorageManager_Delete_PermissionDenied(t *testing.T) { manager := NewFileStorageManager(tmpDir) // Create and store a file - testRegistry := ®istry.Registry{Version: "1.0.0"} + testRegistry := &toolhivetypes.Registry{Version: "1.0.0"} + + // Convert to ServerRegistry + serverRegistry, err := registry.NewServerRegistryFromToolhive(testRegistry) + require.NoError(t, err) + ctx := context.Background() - err := manager.Store(ctx, nil, testRegistry) + err = manager.Store(ctx, nil, serverRegistry) require.NoError(t, err) // Make directory read-only to prevent deletion diff --git a/internal/sources/testutils.go b/internal/sources/testutils.go index 2744290..63a508b 100644 --- a/internal/sources/testutils.go +++ b/internal/sources/testutils.go @@ -5,7 +5,9 @@ import ( "fmt" "time" - "github.com/stacklok/toolhive/pkg/registry" + upstreamv0 "github.com/modelcontextprotocol/registry/pkg/api/v0" + "github.com/modelcontextprotocol/registry/pkg/model" + toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stacklok/toolhive-registry-server/internal/config" ) @@ -13,8 +15,8 @@ import ( // TestRegistryBuilder provides a fluent interface for building test registry data type TestRegistryBuilder struct { format string - registry *registry.Registry - upstreamData []registry.UpstreamServerDetail + registry *toolhivetypes.Registry + upstreamData []upstreamv0.ServerResponse serverCounter int } @@ -27,14 +29,14 @@ func NewTestRegistryBuilder(format string) *TestRegistryBuilder { switch format { case config.SourceFormatToolHive, "": - builder.registry = ®istry.Registry{ + builder.registry = &toolhivetypes.Registry{ Version: "1.0.0", LastUpdated: time.Now().Format(time.RFC3339), - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + Servers: make(map[string]*toolhivetypes.ImageMetadata), + RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), } case config.SourceFormatUpstream: - builder.upstreamData = []registry.UpstreamServerDetail{} + builder.upstreamData = []upstreamv0.ServerResponse{} } return builder @@ -49,8 +51,8 @@ func (b *TestRegistryBuilder) WithServer(name string) *TestRegistryBuilder { switch b.format { case config.SourceFormatToolHive, "": - b.registry.Servers[name] = ®istry.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + b.registry.Servers[name] = &toolhivetypes.ImageMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: name, Description: fmt.Sprintf("Test server description for %s", name), Tier: "Community", @@ -61,8 +63,8 @@ func (b *TestRegistryBuilder) WithServer(name string) *TestRegistryBuilder { }, Image: "test/image:latest", } - b.registry.Servers[name+"-legacy"] = ®istry.ImageMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + b.registry.Servers[name+"-legacy"] = &toolhivetypes.ImageMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: name + "-legacy", Description: fmt.Sprintf("Test server description for %s", name), Tier: "Community", @@ -74,28 +76,36 @@ func (b *TestRegistryBuilder) WithServer(name string) *TestRegistryBuilder { Image: "test/image:latest", } case config.SourceFormatUpstream: - b.upstreamData = append(b.upstreamData, registry.UpstreamServerDetail{ - Server: registry.UpstreamServer{ - Name: name, + b.upstreamData = append(b.upstreamData, upstreamv0.ServerResponse{ + Server: upstreamv0.ServerJSON{ + Schema: "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json", + Name: "io.test/" + name, Description: fmt.Sprintf("Test server description for %s", name), - Packages: []registry.UpstreamPackage{ + Version: "1.0.0", + Packages: []model.Package{ { - RegistryName: "docker", - Name: "test/image", - Version: "latest", + RegistryType: "oci", + Identifier: "test/image:latest", + Transport: model.Transport{ + Type: "stdio", + }, }, }, }, }) - b.upstreamData = append(b.upstreamData, registry.UpstreamServerDetail{ - Server: registry.UpstreamServer{ - Name: name + "-legacy", - Description: fmt.Sprintf("Test server description for %s", name), - Packages: []registry.UpstreamPackage{ + b.upstreamData = append(b.upstreamData, upstreamv0.ServerResponse{ + Server: upstreamv0.ServerJSON{ + Schema: "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json", + Name: "io.test/" + name + "-legacy", + Description: fmt.Sprintf("Test server description for %s-legacy", name), + Version: "1.0.0", + Packages: []model.Package{ { - RegistryName: "docker", - Name: "test/image", - Version: "latest", + RegistryType: "oci", + Identifier: "test/image:latest", + Transport: model.Transport{ + Type: "stdio", + }, }, }, }, @@ -118,8 +128,8 @@ func (b *TestRegistryBuilder) WithRemoteServer(url string) *TestRegistryBuilder url = fmt.Sprintf("https://remote-server-%d.example.com", b.serverCounter-1) } - b.registry.RemoteServers[name] = ®istry.RemoteServerMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + b.registry.RemoteServers[name] = &toolhivetypes.RemoteServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: name, Description: fmt.Sprintf("Test remote server description for %s", name), Tier: "Community", @@ -148,8 +158,8 @@ func (b *TestRegistryBuilder) WithRemoteServerName(name, url string) *TestRegist url = fmt.Sprintf("https://%s.example.com", name) } - b.registry.RemoteServers[name] = ®istry.RemoteServerMetadata{ - BaseServerMetadata: registry.BaseServerMetadata{ + b.registry.RemoteServers[name] = &toolhivetypes.RemoteServerMetadata{ + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ Name: name, Description: fmt.Sprintf("Test remote server description for %s", name), Tier: "Community", @@ -184,10 +194,10 @@ func (b *TestRegistryBuilder) Empty() *TestRegistryBuilder { switch b.format { case config.SourceFormatToolHive, "": // Keep the registry structure but clear servers - b.registry.Servers = make(map[string]*registry.ImageMetadata) - b.registry.RemoteServers = make(map[string]*registry.RemoteServerMetadata) + b.registry.Servers = make(map[string]*toolhivetypes.ImageMetadata) + b.registry.RemoteServers = make(map[string]*toolhivetypes.RemoteServerMetadata) case config.SourceFormatUpstream: - b.upstreamData = []registry.UpstreamServerDetail{} + b.upstreamData = []upstreamv0.ServerResponse{} } return b } @@ -231,7 +241,7 @@ func (b *TestRegistryBuilder) BuildPrettyJSON() []byte { } // GetRegistry returns the built registry (for ToolHive format only) -func (b *TestRegistryBuilder) GetRegistry() *registry.Registry { +func (b *TestRegistryBuilder) GetRegistry() *toolhivetypes.Registry { if b.format == config.SourceFormatToolHive || b.format == "" { return b.registry } @@ -239,7 +249,7 @@ func (b *TestRegistryBuilder) GetRegistry() *registry.Registry { } // GetUpstreamData returns the built upstream data (for Upstream format only) -func (b *TestRegistryBuilder) GetUpstreamData() []registry.UpstreamServerDetail { +func (b *TestRegistryBuilder) GetUpstreamData() []upstreamv0.ServerResponse { if b.format == config.SourceFormatUpstream { return b.upstreamData } diff --git a/internal/sources/testutils_test.go b/internal/sources/testutils_test.go index 9b04259..ecd73dd 100644 --- a/internal/sources/testutils_test.go +++ b/internal/sources/testutils_test.go @@ -4,7 +4,8 @@ import ( "encoding/json" "testing" - "github.com/stacklok/toolhive/pkg/registry" + upstreamv0 "github.com/modelcontextprotocol/registry/pkg/api/v0" + toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stretchr/testify/assert" "github.com/stacklok/toolhive-registry-server/internal/config" @@ -87,13 +88,13 @@ func TestTestRegistryBuilder_WithServer(t *testing.T) { name: "upstream format with explicit name", format: config.SourceFormatUpstream, serverName: "upstream-server", - expectedName: "upstream-server", + expectedName: "io.test/upstream-server", }, { name: "upstream format with empty name", format: config.SourceFormatUpstream, serverName: "", - expectedName: "test-server-1", + expectedName: "io.test/test-server-1", }, { name: "empty format with server", @@ -134,9 +135,8 @@ func TestTestRegistryBuilder_WithServer(t *testing.T) { assert.NotEmpty(t, serverDetail.Server.Description) assert.Len(t, serverDetail.Server.Packages, 1) pkg := serverDetail.Server.Packages[0] - assert.Equal(t, "docker", pkg.RegistryName) - assert.Equal(t, "test/image", pkg.Name) - assert.Equal(t, "latest", pkg.Version) + assert.Equal(t, "oci", pkg.RegistryType) + assert.Equal(t, "test/image:latest", pkg.Identifier) serverDetailLegacy := builder.upstreamData[1] assert.Equal(t, tt.expectedName+"-legacy", serverDetailLegacy.Server.Name) } @@ -195,7 +195,7 @@ func TestTestRegistryBuilder_WithRemoteServer(t *testing.T) { if tt.shouldAdd { assert.Len(t, builder.registry.RemoteServers, 1) - var remoteServer *registry.RemoteServerMetadata + var remoteServer *toolhivetypes.RemoteServerMetadata for _, server := range builder.registry.RemoteServers { remoteServer = server break @@ -478,14 +478,14 @@ func TestTestRegistryBuilder_BuildJSON(t *testing.T) { switch tt.format { case config.SourceFormatToolHive, "": // Should be a registry object - var registry registry.Registry + var registry toolhivetypes.Registry err = json.Unmarshal(jsonData, ®istry) assert.NoError(t, err) assert.Equal(t, "1.0.0", registry.Version) assert.Len(t, registry.Servers, 2) case config.SourceFormatUpstream: - // Should be an array of server details - var upstreamData []registry.UpstreamServerDetail + // Should be an array of server responses + var upstreamData []upstreamv0.ServerResponse err = json.Unmarshal(jsonData, &upstreamData) assert.NoError(t, err) assert.Len(t, upstreamData, 2) @@ -510,7 +510,7 @@ func TestTestRegistryBuilder_BuildPrettyJSON(t *testing.T) { assert.Greater(t, len(prettyJSON), len(regularJSON)) // Both should unmarshal to the same data - var prettyData, regularData registry.Registry + var prettyData, regularData toolhivetypes.Registry err1 := json.Unmarshal(prettyJSON, &prettyData) err2 := json.Unmarshal(regularJSON, ®ularData) assert.NoError(t, err1) diff --git a/internal/sources/types.go b/internal/sources/types.go index 53e6643..e1cdd55 100644 --- a/internal/sources/types.go +++ b/internal/sources/types.go @@ -5,15 +5,18 @@ import ( "encoding/json" "fmt" - "github.com/stacklok/toolhive/pkg/registry" + upstreamv0 "github.com/modelcontextprotocol/registry/pkg/api/v0" + toolhiveregistry "github.com/stacklok/toolhive/pkg/registry" + toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" - "github.com/stacklok/toolhive-registry-server/internal/config" + "github.com/stacklok/toolhive-registry-server/pkg/config" + "github.com/stacklok/toolhive-registry-server/pkg/registry" ) // SourceDataValidator is an interface for validating registry source configurations type SourceDataValidator interface { - // ValidateData validates raw data and returns a parsed Registry - ValidateData(data []byte, format string) (*registry.Registry, error) + // ValidateData validates raw data and returns a parsed ServerRegistry + ValidateData(data []byte, format string) (*registry.ServerRegistry, error) } //go:generate mockgen -destination=mocks/mock_source_handler.go -package=mocks -source=types.go SourceHandler,SourceHandlerFactory @@ -32,8 +35,8 @@ type SourceHandler interface { // FetchResult contains the result of a fetch operation type FetchResult struct { - // Registry is the parsed registry data (replaces raw Data field) - Registry *registry.Registry + // Registry is the parsed registry data in unified ServerRegistry format + Registry *registry.ServerRegistry // Hash is the SHA256 hash of the serialized data for change detection Hash string @@ -45,10 +48,13 @@ type FetchResult struct { Format string } -// NewFetchResult creates a new FetchResult from a Registry instance and pre-calculated hash +// NewFetchResult creates a new FetchResult from a ServerRegistry instance and pre-calculated hash // The hash should be calculated by the source handler to ensure consistency with CurrentHash -func NewFetchResult(reg *registry.Registry, hash string, format string) *FetchResult { - serverCount := len(reg.Servers) + len(reg.RemoteServers) +func NewFetchResult(reg *registry.ServerRegistry, hash string, format string) *FetchResult { + serverCount := 0 + if reg != nil { + serverCount = len(reg.Servers) + } return &FetchResult{ Registry: reg, @@ -72,8 +78,8 @@ func NewSourceDataValidator() SourceDataValidator { return &DefaultSourceDataValidator{} } -// ValidateData validates raw data and returns a parsed Registry -func (*DefaultSourceDataValidator) ValidateData(data []byte, format string) (*registry.Registry, error) { +// ValidateData validates raw data and returns a parsed ServerRegistry +func (*DefaultSourceDataValidator) ValidateData(data []byte, format string) (*registry.ServerRegistry, error) { if len(data) == 0 { return nil, fmt.Errorf("data cannot be empty") } @@ -88,68 +94,53 @@ func (*DefaultSourceDataValidator) ValidateData(data []byte, format string) (*re } } -// validateToolhiveFormatAndParse validates data against ToolHive registry format and returns parsed Registry -func validateToolhiveFormatAndParse(data []byte) (*registry.Registry, error) { - // Use the existing schema validation from pkg/registry - if err := registry.ValidateRegistrySchema(data); err != nil { +// validateToolhiveFormatAndParse validates data against ToolHive registry format and returns parsed ServerRegistry +func validateToolhiveFormatAndParse(data []byte) (*registry.ServerRegistry, error) { + // Use the existing schema validation from toolhive package + if err := toolhiveregistry.ValidateRegistrySchema(data); err != nil { return nil, err } - // Parse the validated data - var reg registry.Registry - if err := json.Unmarshal(data, ®); err != nil { + // Parse the validated data as ToolHive Registry + var toolhiveReg toolhivetypes.Registry + if err := json.Unmarshal(data, &toolhiveReg); err != nil { return nil, fmt.Errorf("failed to parse ToolHive registry format: %w", err) } - return ®, nil + // Convert to ServerRegistry using constructor + serverReg, err := registry.NewServerRegistryFromToolhive(&toolhiveReg) + if err != nil { + return nil, fmt.Errorf("failed to convert to ServerRegistry: %w", err) + } + + return serverReg, nil } -// validateUpstreamFormatAndParse validates data against upstream registry format and returns converted Registry -func validateUpstreamFormatAndParse(data []byte) (*registry.Registry, error) { - // Parse as upstream format to validate structure - var upstreamServers []registry.UpstreamServerDetail - if err := json.Unmarshal(data, &upstreamServers); err != nil { +// validateUpstreamFormatAndParse validates data against upstream registry format and returns ServerRegistry +func validateUpstreamFormatAndParse(data []byte) (*registry.ServerRegistry, error) { + // Parse as upstream ServerResponse array to validate structure + var responses []upstreamv0.ServerResponse + if err := json.Unmarshal(data, &responses); err != nil { return nil, fmt.Errorf("invalid upstream format: %w", err) } // Basic validation - ensure we have at least one server and required fields - if len(upstreamServers) == 0 { + if len(responses) == 0 { return nil, fmt.Errorf("upstream registry must contain at least one server") } - for i, server := range upstreamServers { - if server.Server.Name == "" { + // Extract ServerJSON from responses for validation and conversion + servers := make([]upstreamv0.ServerJSON, len(responses)) + for i, response := range responses { + servers[i] = response.Server + if response.Server.Name == "" { return nil, fmt.Errorf("server at index %d: name is required", i) } - if server.Server.Description == "" { - return nil, fmt.Errorf("server at index %d (%s): description is required", i, server.Server.Name) - } - } - - // Convert upstream format to ToolHive Registry format - toolhiveRegistry := ®istry.Registry{ - Version: "1.0", - LastUpdated: "", // Will be set during sync - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: make(map[string]*registry.RemoteServerMetadata), - } - - for _, upstreamServer := range upstreamServers { - serverMetadata, err := registry.ConvertUpstreamToToolhive(&upstreamServer) - if err != nil { - return nil, fmt.Errorf("failed to convert server %s: %w", upstreamServer.Server.Name, err) - } - - // Add to appropriate map based on server type - switch server := serverMetadata.(type) { - case *registry.ImageMetadata: - toolhiveRegistry.Servers[upstreamServer.Server.Name] = server - case *registry.RemoteServerMetadata: - toolhiveRegistry.RemoteServers[upstreamServer.Server.Name] = server - default: - return nil, fmt.Errorf("unknown server type for %s", upstreamServer.Server.Name) + if response.Server.Description == "" { + return nil, fmt.Errorf("server at index %d (%s): description is required", i, response.Server.Name) } } - return toolhiveRegistry, nil + // Wrap in ServerRegistry using constructor + return registry.NewServerRegistryFromUpstream(servers), nil } diff --git a/internal/sources/types_test.go b/internal/sources/types_test.go index 08c5a42..9ed65ce 100644 --- a/internal/sources/types_test.go +++ b/internal/sources/types_test.go @@ -3,10 +3,12 @@ package sources import ( "testing" - "github.com/stacklok/toolhive/pkg/registry" + toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" - "github.com/stacklok/toolhive-registry-server/internal/config" + "github.com/stacklok/toolhive-registry-server/pkg/config" + "github.com/stacklok/toolhive-registry-server/pkg/registry" ) func TestNewFetchResult(t *testing.T) { @@ -14,39 +16,39 @@ func TestNewFetchResult(t *testing.T) { tests := []struct { name string - registryData *registry.Registry + registryData *toolhivetypes.Registry hash string format string }{ { name: "empty registry", - registryData: ®istry.Registry{ + registryData: &toolhivetypes.Registry{ Version: "1.0.0", - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + Servers: make(map[string]*toolhivetypes.ImageMetadata), + RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), }, hash: "abcd1234", format: config.SourceFormatToolHive, }, { name: "registry with servers", - registryData: ®istry.Registry{ + registryData: &toolhivetypes.Registry{ Version: "1.0.0", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*toolhivetypes.ImageMetadata{ "server1": {}, "server2": {}, }, - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), }, hash: "efgh5678", format: config.SourceFormatToolHive, }, { name: "registry with remote servers", - registryData: ®istry.Registry{ + registryData: &toolhivetypes.Registry{ Version: "1.0.0", - Servers: make(map[string]*registry.ImageMetadata), - RemoteServers: map[string]*registry.RemoteServerMetadata{ + Servers: make(map[string]*toolhivetypes.ImageMetadata), + RemoteServers: map[string]*toolhivetypes.RemoteServerMetadata{ "remote1": {}, }, }, @@ -59,13 +61,22 @@ func TestNewFetchResult(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - result := NewFetchResult(tt.registryData, tt.hash, tt.format) + // Convert ToolHive Registry to ServerRegistry + serverReg, err := registry.NewServerRegistryFromToolhive(tt.registryData) + require.NoError(t, err, "Failed to convert toolhive registry to server registry") + + result := NewFetchResult(serverReg, tt.hash, tt.format) expectedServerCount := len(tt.registryData.Servers) + len(tt.registryData.RemoteServers) assert.Equal(t, expectedServerCount, result.ServerCount) assert.Equal(t, tt.hash, result.Hash) assert.Equal(t, tt.format, result.Format) - assert.Equal(t, tt.registryData, result.Registry) + + // Verify registry by converting back + convertedBack, err := result.Registry.ToToolhive() + require.NoError(t, err, "Failed to convert server registry back to toolhive") + assert.Equal(t, len(tt.registryData.Servers), len(convertedBack.Servers)) + assert.Equal(t, len(tt.registryData.RemoteServers), len(convertedBack.RemoteServers)) }) } } @@ -73,22 +84,26 @@ func TestNewFetchResult(t *testing.T) { func TestFetchResultHashConsistency(t *testing.T) { t.Parallel() - registryData := ®istry.Registry{ + registryData := &toolhivetypes.Registry{ Version: "1.0.0", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*toolhivetypes.ImageMetadata{ "server1": {}, "server2": {}, "server3": {}, "server4": {}, "server5": {}, }, - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), } hash := "consistent-hash-value" format := config.SourceFormatToolHive - result1 := NewFetchResult(registryData, hash, format) - result2 := NewFetchResult(registryData, hash, format) + // Convert to ServerRegistry + serverReg, err := registry.NewServerRegistryFromToolhive(registryData) + require.NoError(t, err) + + result1 := NewFetchResult(serverReg, hash, format) + result2 := NewFetchResult(serverReg, hash, format) // Same data should produce same results assert.Equal(t, result1.Hash, result2.Hash) @@ -100,28 +115,34 @@ func TestFetchResultHashConsistency(t *testing.T) { func TestFetchResultHashDifference(t *testing.T) { t.Parallel() - registryData1 := ®istry.Registry{ + registryData1 := &toolhivetypes.Registry{ Version: "1.0.0", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*toolhivetypes.ImageMetadata{ "server1": {}, }, - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), } - registryData2 := ®istry.Registry{ + registryData2 := &toolhivetypes.Registry{ Version: "1.0.0", - Servers: map[string]*registry.ImageMetadata{ + Servers: map[string]*toolhivetypes.ImageMetadata{ "server2": {}, }, - RemoteServers: make(map[string]*registry.RemoteServerMetadata), + RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), } hash1 := "hash-for-data1" hash2 := "hash-for-data2" format := config.SourceFormatToolHive - result1 := NewFetchResult(registryData1, hash1, format) - result2 := NewFetchResult(registryData2, hash2, format) + // Convert to ServerRegistry + serverReg1, err := registry.NewServerRegistryFromToolhive(registryData1) + require.NoError(t, err) + serverReg2, err := registry.NewServerRegistryFromToolhive(registryData2) + require.NoError(t, err) + + result1 := NewFetchResult(serverReg1, hash1, format) + result2 := NewFetchResult(serverReg2, hash2, format) // Different data should produce different hashes assert.NotEqual(t, result1.Hash, result2.Hash) diff --git a/internal/sync/manager.go b/internal/sync/manager.go index 6e9e4a6..4e21c42 100644 --- a/internal/sync/manager.go +++ b/internal/sync/manager.go @@ -10,10 +10,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" - "github.com/stacklok/toolhive-registry-server/internal/config" - "github.com/stacklok/toolhive-registry-server/internal/filtering" - sources2 "github.com/stacklok/toolhive-registry-server/internal/sources" - "github.com/stacklok/toolhive-registry-server/internal/status" + "github.com/stacklok/toolhive-registry-server/pkg/config" + "github.com/stacklok/toolhive-registry-server/pkg/filtering" + "github.com/stacklok/toolhive-registry-server/pkg/registry" + "github.com/stacklok/toolhive-registry-server/pkg/status" ) // Result contains the result of a successful sync operation @@ -357,7 +357,19 @@ func (s *DefaultSyncManager) applyFilteringIfConfigured( "hasNameFilters", cfg.Filter.Names != nil, "hasTagFilters", cfg.Filter.Tags != nil) - filteredRegistry, err := s.filterService.ApplyFilters(ctx, fetchResult.Registry, cfg.Filter) + // Convert ServerRegistry to ToolHive format for filtering + toolhiveReg, err := fetchResult.Registry.ToToolhive() + if err != nil { + ctxLogger.Error(err, "Failed to convert to ToolHive format for filtering") + return &Error{ + Err: err, + Message: fmt.Sprintf("Conversion to ToolHive failed: %v", err), + ConditionType: ConditionSyncSuccessful, + ConditionReason: conditionReasonFetchFailed, + } + } + + filteredToolhiveReg, err := s.filterService.ApplyFilters(ctx, toolhiveReg, cfg.Filter) if err != nil { ctxLogger.Error(err, "Registry filtering failed") return &Error{ @@ -368,10 +380,22 @@ func (s *DefaultSyncManager) applyFilteringIfConfigured( } } + // Convert filtered ToolHive registry back to ServerRegistry + filteredServerReg, err := registry.NewServerRegistryFromToolhive(filteredToolhiveReg) + if err != nil { + ctxLogger.Error(err, "Failed to convert filtered registry to ServerRegistry") + return &Error{ + Err: err, + Message: fmt.Sprintf("Conversion to ServerRegistry failed: %v", err), + ConditionType: ConditionSyncSuccessful, + ConditionReason: conditionReasonFetchFailed, + } + } + // Update fetch result with filtered data originalServerCount := fetchResult.ServerCount - fetchResult.Registry = filteredRegistry - fetchResult.ServerCount = len(filteredRegistry.Servers) + len(filteredRegistry.RemoteServers) + fetchResult.Registry = filteredServerReg + fetchResult.ServerCount = len(filteredServerReg.Servers) ctxLogger.Info("Registry filtering completed", "originalServerCount", originalServerCount, diff --git a/pkg/registry/server_registry.go b/pkg/registry/server_registry.go new file mode 100644 index 0000000..7853851 --- /dev/null +++ b/pkg/registry/server_registry.go @@ -0,0 +1,145 @@ +// Package registry provides the unified internal registry format. +// +// ServerRegistry is the single source of truth for registry data, storing servers +// in upstream MCP ServerJSON format while maintaining ToolHive-compatible metadata +// fields for backward compatibility and versioning. +package registry + +import ( + "fmt" + "strings" + "time" + + upstreamv0 "github.com/modelcontextprotocol/registry/pkg/api/v0" + "github.com/stacklok/toolhive/pkg/registry/converters" + toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" +) + +// ServerRegistry is the unified internal registry format. +// It stores servers in upstream ServerJSON format while maintaining +// ToolHive-compatible metadata fields for backward compatibility. +type ServerRegistry struct { + // Version is the schema version (ToolHive compatibility) + Version string `json:"version"` + + // LastUpdated is the timestamp when registry was last updated (ToolHive compatibility) + LastUpdated string `json:"last_updated"` + + // Servers contains the server definitions in upstream MCP format + Servers []upstreamv0.ServerJSON `json:"servers"` +} + +// NewServerRegistryFromUpstream creates a ServerRegistry from upstream ServerJSON array. +// This is used when ingesting data from upstream MCP Registry API endpoints. +func NewServerRegistryFromUpstream(servers []upstreamv0.ServerJSON) *ServerRegistry { + return &ServerRegistry{ + Version: "1.0.0", + LastUpdated: time.Now().Format(time.RFC3339), + Servers: servers, + } +} + +// NewServerRegistryFromToolhive creates a ServerRegistry from ToolHive Registry. +// This converts ToolHive format to upstream ServerJSON using the converters package. +// Used when ingesting data from ToolHive-format sources (Git, File, API). +func NewServerRegistryFromToolhive(toolhiveReg *toolhivetypes.Registry) (*ServerRegistry, error) { + if toolhiveReg == nil { + return nil, fmt.Errorf("toolhive registry cannot be nil") + } + + servers := make([]upstreamv0.ServerJSON, 0, len(toolhiveReg.Servers)+len(toolhiveReg.RemoteServers)) + + // Convert container servers using converters package + for name, imgMeta := range toolhiveReg.Servers { + serverJSON, err := converters.ImageMetadataToServerJSON(name, imgMeta) + if err != nil { + return nil, fmt.Errorf("failed to convert server %s: %w", name, err) + } + servers = append(servers, *serverJSON) + } + + // Convert remote servers using converters package + for name, remoteMeta := range toolhiveReg.RemoteServers { + serverJSON, err := converters.RemoteServerMetadataToServerJSON(name, remoteMeta) + if err != nil { + return nil, fmt.Errorf("failed to convert remote server %s: %w", name, err) + } + servers = append(servers, *serverJSON) + } + + return &ServerRegistry{ + Version: toolhiveReg.Version, + LastUpdated: toolhiveReg.LastUpdated, + Servers: servers, + }, nil +} + +// ToToolhive converts ServerRegistry back to ToolHive Registry format. +// Used for backward compatibility with v0 API. +func (sr *ServerRegistry) ToToolhive() (*toolhivetypes.Registry, error) { + if sr == nil { + return nil, fmt.Errorf("server registry cannot be nil") + } + + toolhiveReg := &toolhivetypes.Registry{ + Version: sr.Version, + LastUpdated: sr.LastUpdated, + Servers: make(map[string]*toolhivetypes.ImageMetadata), + RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), + } + + for i := range sr.Servers { + serverJSON := &sr.Servers[i] + name := extractSimpleName(serverJSON.Name) + + // Detect server type by presence of packages vs remotes + if len(serverJSON.Packages) > 0 { + // Container server + imgMeta, err := converters.ServerJSONToImageMetadata(serverJSON) + if err != nil { + return nil, fmt.Errorf("failed to convert server %s: %w", serverJSON.Name, err) + } + toolhiveReg.Servers[name] = imgMeta + } else if len(serverJSON.Remotes) > 0 { + // Remote server + remoteMeta, err := converters.ServerJSONToRemoteServerMetadata(serverJSON) + if err != nil { + return nil, fmt.Errorf("failed to convert remote server %s: %w", serverJSON.Name, err) + } + toolhiveReg.RemoteServers[name] = remoteMeta + } + // Note: Servers with neither packages nor remotes are skipped + // This shouldn't happen with valid ServerJSON data + } + + return toolhiveReg, nil +} + +// GetServerByName retrieves a server by its name. +// Supports both reverse-DNS format (e.g., "io.github.user/server") and simple names (e.g., "server"). +func (sr *ServerRegistry) GetServerByName(name string) (*upstreamv0.ServerJSON, bool) { + if sr == nil { + return nil, false + } + + for i := range sr.Servers { + serverName := sr.Servers[i].Name + if serverName == name || extractSimpleName(serverName) == name { + return &sr.Servers[i], true + } + } + return nil, false +} + +// extractSimpleName extracts the simple server name from reverse-DNS format. +// Examples: +// - "io.github.user/server" -> "server" +// - "com.example/my-server" -> "my-server" +// - "simple-name" -> "simple-name" (no change if not reverse-DNS) +func extractSimpleName(reverseDNS string) string { + idx := strings.LastIndex(reverseDNS, "/") + if idx >= 0 && idx < len(reverseDNS)-1 { + return reverseDNS[idx+1:] + } + return reverseDNS +} diff --git a/pkg/registry/server_registry_test.go b/pkg/registry/server_registry_test.go new file mode 100644 index 0000000..774632b --- /dev/null +++ b/pkg/registry/server_registry_test.go @@ -0,0 +1,307 @@ +package registry + +import ( + "testing" + "time" + + upstreamv0 "github.com/modelcontextprotocol/registry/pkg/api/v0" + "github.com/modelcontextprotocol/registry/pkg/model" + toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewServerRegistryFromToolhive(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + toolhiveReg *toolhivetypes.Registry + expectError bool + validate func(*testing.T, *ServerRegistry) + }{ + { + name: "successful conversion with container servers", + toolhiveReg: &toolhivetypes.Registry{ + Version: "1.0.0", + LastUpdated: "2024-01-01T00:00:00Z", + Servers: map[string]*toolhivetypes.ImageMetadata{ + "test-server": { + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ + Name: "test-server", + Description: "A test server", + Tier: "Community", + Status: "Active", + Transport: "stdio", + Tools: []string{"test_tool"}, + }, + Image: "test/image:latest", + }, + }, + RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), + }, + expectError: false, + validate: func(t *testing.T, sr *ServerRegistry) { + t.Helper() + assert.Equal(t, "1.0.0", sr.Version) + assert.Equal(t, "2024-01-01T00:00:00Z", sr.LastUpdated) + assert.Len(t, sr.Servers, 1) + assert.Contains(t, sr.Servers[0].Name, "test-server") + assert.Equal(t, "A test server", sr.Servers[0].Description) + }, + }, + { + name: "successful conversion with remote servers", + toolhiveReg: &toolhivetypes.Registry{ + Version: "1.0.0", + LastUpdated: "2024-01-01T00:00:00Z", + Servers: make(map[string]*toolhivetypes.ImageMetadata), + RemoteServers: map[string]*toolhivetypes.RemoteServerMetadata{ + "remote-server": { + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ + Name: "remote-server", + Description: "A remote server", + Tier: "Community", + Status: "Active", + Transport: "sse", + Tools: []string{"remote_tool"}, + }, + URL: "https://example.com", + }, + }, + }, + expectError: false, + validate: func(t *testing.T, sr *ServerRegistry) { + t.Helper() + assert.Len(t, sr.Servers, 1) + assert.Contains(t, sr.Servers[0].Name, "remote-server") + }, + }, + { + name: "empty registry", + toolhiveReg: &toolhivetypes.Registry{ + Version: "1.0.0", + LastUpdated: "2024-01-01T00:00:00Z", + Servers: make(map[string]*toolhivetypes.ImageMetadata), + RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), + }, + expectError: false, + validate: func(t *testing.T, sr *ServerRegistry) { + t.Helper() + assert.Empty(t, sr.Servers) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result, err := NewServerRegistryFromToolhive(tt.toolhiveReg) + + if tt.expectError { + assert.Error(t, err) + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.NotNil(t, result) + if tt.validate != nil { + tt.validate(t, result) + } + } + }) + } +} + +func TestNewServerRegistryFromUpstream(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + servers []upstreamv0.ServerJSON + validate func(*testing.T, *ServerRegistry) + }{ + { + name: "create from upstream servers", + servers: []upstreamv0.ServerJSON{ + { + Schema: "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json", + Name: "io.test/server1", + Description: "Test server 1", + Version: "1.0.0", + Packages: []model.Package{ + { + RegistryType: "oci", + Identifier: "test/image:latest", + Transport: model.Transport{Type: "stdio"}, + }, + }, + }, + }, + validate: func(t *testing.T, sr *ServerRegistry) { + t.Helper() + assert.Equal(t, "1.0.0", sr.Version) + assert.NotEmpty(t, sr.LastUpdated) + assert.Len(t, sr.Servers, 1) + assert.Equal(t, "io.test/server1", sr.Servers[0].Name) + }, + }, + { + name: "create from empty slice", + servers: []upstreamv0.ServerJSON{}, + validate: func(t *testing.T, sr *ServerRegistry) { + t.Helper() + assert.Empty(t, sr.Servers) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := NewServerRegistryFromUpstream(tt.servers) + + assert.NotNil(t, result) + if tt.validate != nil { + tt.validate(t, result) + } + }) + } +} + +func TestServerRegistry_ToToolhive(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + serverReg *ServerRegistry + expectError bool + validateFunc func(*testing.T, *toolhivetypes.Registry) + }{ + { + name: "convert to toolhive with servers", + serverReg: &ServerRegistry{ + Version: "1.0.0", + LastUpdated: "2024-01-01T00:00:00Z", + Servers: []upstreamv0.ServerJSON{ + { + Schema: "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json", + Name: "io.test/server1", + Description: "Test server", + Version: "1.0.0", + Packages: []model.Package{ + { + RegistryType: "oci", + Identifier: "test/image:latest", + Transport: model.Transport{Type: "stdio"}, + }, + }, + }, + }, + }, + expectError: false, + validateFunc: func(t *testing.T, reg *toolhivetypes.Registry) { + t.Helper() + assert.Equal(t, "1.0.0", reg.Version) + assert.Equal(t, "2024-01-01T00:00:00Z", reg.LastUpdated) + assert.Len(t, reg.Servers, 1) + }, + }, + { + name: "convert empty registry", + serverReg: &ServerRegistry{ + Version: "1.0.0", + LastUpdated: "2024-01-01T00:00:00Z", + Servers: []upstreamv0.ServerJSON{}, + }, + expectError: false, + validateFunc: func(t *testing.T, reg *toolhivetypes.Registry) { + t.Helper() + assert.Empty(t, reg.Servers) + assert.Empty(t, reg.RemoteServers) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result, err := tt.serverReg.ToToolhive() + + if tt.expectError { + assert.Error(t, err) + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.NotNil(t, result) + if tt.validateFunc != nil { + tt.validateFunc(t, result) + } + } + }) + } +} + +func TestServerRegistry_RoundTripConversion(t *testing.T) { + t.Parallel() + + originalToolhive := &toolhivetypes.Registry{ + Version: "1.0.0", + LastUpdated: "2024-01-01T00:00:00Z", + Servers: map[string]*toolhivetypes.ImageMetadata{ + "test-server": { + BaseServerMetadata: toolhivetypes.BaseServerMetadata{ + Name: "test-server", + Description: "A test server", + Tier: "Community", + Status: "Active", + Transport: "stdio", + Tools: []string{"test_tool"}, + }, + Image: "test/image:latest", + }, + }, + RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), + } + + // Convert to ServerRegistry + serverReg, err := NewServerRegistryFromToolhive(originalToolhive) + require.NoError(t, err) + require.NotNil(t, serverReg) + + // Convert back to ToolHive + convertedBack, err := serverReg.ToToolhive() + require.NoError(t, err) + require.NotNil(t, convertedBack) + + // Verify key fields match + assert.Equal(t, originalToolhive.Version, convertedBack.Version) + assert.Equal(t, originalToolhive.LastUpdated, convertedBack.LastUpdated) + assert.Len(t, convertedBack.Servers, 1) +} + +func TestNewServerRegistryFromUpstream_DefaultValues(t *testing.T) { + t.Parallel() + + servers := []upstreamv0.ServerJSON{ + { + Schema: "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json", + Name: "io.test/server1", + Description: "Test server", + Version: "1.0.0", + }, + } + + result := NewServerRegistryFromUpstream(servers) + + // Verify defaults + assert.Equal(t, "1.0.0", result.Version) + assert.NotEmpty(t, result.LastUpdated) + + // Verify timestamp is recent (within last minute) + parsedTime, err := time.Parse(time.RFC3339, result.LastUpdated) + require.NoError(t, err) + assert.WithinDuration(t, time.Now(), parsedTime, time.Minute) +} From 170eaf17f3ee66c86054520a57657813d38cf36d Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Thu, 13 Nov 2025 15:40:58 +0100 Subject: [PATCH 2/4] rebased :( --- internal/app/builder.go | 14 ++++++------- {pkg => internal}/registry/server_registry.go | 0 .../registry/server_registry_test.go | 0 internal/service/file_provider_test.go | 6 +++--- internal/sources/api_toolhive.go | 2 +- internal/sources/git_test.go | 6 +++--- internal/sources/mocks/mock_source_handler.go | 6 +++--- .../sources/mocks/mock_storage_manager.go | 4 ++-- internal/sources/storage_manager.go | 4 ++-- internal/sources/storage_manager_test.go | 2 +- internal/sources/types.go | 4 ++-- internal/sources/types_test.go | 4 ++-- internal/sync/manager.go | 21 ++++++++++--------- 13 files changed, 37 insertions(+), 36 deletions(-) rename {pkg => internal}/registry/server_registry.go (100%) rename {pkg => internal}/registry/server_registry_test.go (100%) diff --git a/internal/app/builder.go b/internal/app/builder.go index 010c4a7..064d40d 100644 --- a/internal/app/builder.go +++ b/internal/app/builder.go @@ -15,7 +15,7 @@ import ( "github.com/stacklok/toolhive-registry-server/internal/api" "github.com/stacklok/toolhive-registry-server/internal/config" "github.com/stacklok/toolhive-registry-server/internal/service" - sources2 "github.com/stacklok/toolhive-registry-server/internal/sources" + "github.com/stacklok/toolhive-registry-server/internal/sources" "github.com/stacklok/toolhive-registry-server/internal/status" pkgsync "github.com/stacklok/toolhive-registry-server/internal/sync" "github.com/stacklok/toolhive-registry-server/internal/sync/coordinator" @@ -41,8 +41,8 @@ type registryAppConfig struct { config *config.Config // Optional component overrides (primarily for testing) - sourceHandlerFactory sources2.SourceHandlerFactory - storageManager sources2.StorageManager + sourceHandlerFactory sources.SourceHandlerFactory + storageManager sources.StorageManager statusPersistence status.StatusPersistence syncManager pkgsync.Manager registryProvider service.RegistryDataProvider @@ -184,7 +184,7 @@ func WithDataDirectory(dir string) RegistryAppOptions { } // WithSourceHandlerFactory allows injecting a custom source handler factory (for testing) -func WithSourceHandlerFactory(factory sources2.SourceHandlerFactory) RegistryAppOptions { +func WithSourceHandlerFactory(factory sources.SourceHandlerFactory) RegistryAppOptions { return func(cfg *registryAppConfig) error { cfg.sourceHandlerFactory = factory return nil @@ -192,7 +192,7 @@ func WithSourceHandlerFactory(factory sources2.SourceHandlerFactory) RegistryApp } // WithStorageManager allows injecting a custom storage manager (for testing) -func WithStorageManager(sm sources2.StorageManager) RegistryAppOptions { +func WithStorageManager(sm sources.StorageManager) RegistryAppOptions { return func(cfg *registryAppConfig) error { cfg.storageManager = sm return nil @@ -239,7 +239,7 @@ func buildSyncComponents( // Build source handler factory if b.sourceHandlerFactory == nil { - b.sourceHandlerFactory = sources2.NewSourceHandlerFactory() + b.sourceHandlerFactory = sources.NewSourceHandlerFactory() } // Build storage manager @@ -248,7 +248,7 @@ func buildSyncComponents( if err := os.MkdirAll(b.dataDir, 0750); err != nil { return nil, fmt.Errorf("failed to create data directory %s: %w", b.dataDir, err) } - b.storageManager = sources2.NewFileStorageManager(b.dataDir) + b.storageManager = sources.NewFileStorageManager(b.dataDir) } // Build status persistence diff --git a/pkg/registry/server_registry.go b/internal/registry/server_registry.go similarity index 100% rename from pkg/registry/server_registry.go rename to internal/registry/server_registry.go diff --git a/pkg/registry/server_registry_test.go b/internal/registry/server_registry_test.go similarity index 100% rename from pkg/registry/server_registry_test.go rename to internal/registry/server_registry_test.go diff --git a/internal/service/file_provider_test.go b/internal/service/file_provider_test.go index 508877b..c930883 100644 --- a/internal/service/file_provider_test.go +++ b/internal/service/file_provider_test.go @@ -10,9 +10,9 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" - "github.com/stacklok/toolhive-registry-server/pkg/config" - "github.com/stacklok/toolhive-registry-server/pkg/registry" - sourcesmocks "github.com/stacklok/toolhive-registry-server/pkg/sources/mocks" + "github.com/stacklok/toolhive-registry-server/internal/config" + "github.com/stacklok/toolhive-registry-server/internal/registry" + sourcesmocks "github.com/stacklok/toolhive-registry-server/internal/sources/mocks" ) func TestFileRegistryDataProvider_GetRegistryData(t *testing.T) { diff --git a/internal/sources/api_toolhive.go b/internal/sources/api_toolhive.go index d12332e..8fca089 100644 --- a/internal/sources/api_toolhive.go +++ b/internal/sources/api_toolhive.go @@ -15,7 +15,7 @@ import ( "github.com/stacklok/toolhive-registry-server/internal/config" "github.com/stacklok/toolhive-registry-server/internal/httpclient" - "github.com/stacklok/toolhive-registry-server/pkg/registry" + "github.com/stacklok/toolhive-registry-server/internal/registry" ) // ToolHiveAPIHandler handles registry data from ToolHive Registry API endpoints diff --git a/internal/sources/git_test.go b/internal/sources/git_test.go index e2c71dd..5c48e38 100644 --- a/internal/sources/git_test.go +++ b/internal/sources/git_test.go @@ -12,9 +12,9 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/stacklok/toolhive-registry-server/pkg/config" - "github.com/stacklok/toolhive-registry-server/pkg/git" - "github.com/stacklok/toolhive-registry-server/pkg/registry" + "github.com/stacklok/toolhive-registry-server/internal/config" + "github.com/stacklok/toolhive-registry-server/internal/git" + "github.com/stacklok/toolhive-registry-server/internal/registry" ) const ( diff --git a/internal/sources/mocks/mock_source_handler.go b/internal/sources/mocks/mock_source_handler.go index f549919..a6dc2a5 100644 --- a/internal/sources/mocks/mock_source_handler.go +++ b/internal/sources/mocks/mock_source_handler.go @@ -13,9 +13,9 @@ import ( context "context" reflect "reflect" - config "github.com/stacklok/toolhive-registry-server/pkg/config" - registry "github.com/stacklok/toolhive-registry-server/pkg/registry" - sources "github.com/stacklok/toolhive-registry-server/pkg/sources" + config "github.com/stacklok/toolhive-registry-server/internal/config" + registry "github.com/stacklok/toolhive-registry-server/internal/registry" + sources "github.com/stacklok/toolhive-registry-server/internal/sources" gomock "go.uber.org/mock/gomock" ) diff --git a/internal/sources/mocks/mock_storage_manager.go b/internal/sources/mocks/mock_storage_manager.go index 4bce0ed..a0e820a 100644 --- a/internal/sources/mocks/mock_storage_manager.go +++ b/internal/sources/mocks/mock_storage_manager.go @@ -13,8 +13,8 @@ import ( context "context" reflect "reflect" - config "github.com/stacklok/toolhive-registry-server/pkg/config" - registry "github.com/stacklok/toolhive-registry-server/pkg/registry" + config "github.com/stacklok/toolhive-registry-server/internal/config" + registry "github.com/stacklok/toolhive-registry-server/internal/registry" gomock "go.uber.org/mock/gomock" ) diff --git a/internal/sources/storage_manager.go b/internal/sources/storage_manager.go index c8cc962..4a3fe38 100644 --- a/internal/sources/storage_manager.go +++ b/internal/sources/storage_manager.go @@ -7,8 +7,8 @@ import ( "os" "path/filepath" - "github.com/stacklok/toolhive-registry-server/pkg/config" - "github.com/stacklok/toolhive-registry-server/pkg/registry" + "github.com/stacklok/toolhive-registry-server/internal/config" + "github.com/stacklok/toolhive-registry-server/internal/registry" ) const ( diff --git a/internal/sources/storage_manager_test.go b/internal/sources/storage_manager_test.go index a46a8f7..f4099aa 100644 --- a/internal/sources/storage_manager_test.go +++ b/internal/sources/storage_manager_test.go @@ -9,7 +9,7 @@ import ( toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stretchr/testify/require" - "github.com/stacklok/toolhive-registry-server/pkg/registry" + "github.com/stacklok/toolhive-registry-server/internal/registry" ) func TestFileStorageManager_StoreAndGet(t *testing.T) { diff --git a/internal/sources/types.go b/internal/sources/types.go index e1cdd55..47a243e 100644 --- a/internal/sources/types.go +++ b/internal/sources/types.go @@ -9,8 +9,8 @@ import ( toolhiveregistry "github.com/stacklok/toolhive/pkg/registry" toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" - "github.com/stacklok/toolhive-registry-server/pkg/config" - "github.com/stacklok/toolhive-registry-server/pkg/registry" + "github.com/stacklok/toolhive-registry-server/internal/config" + "github.com/stacklok/toolhive-registry-server/internal/registry" ) // SourceDataValidator is an interface for validating registry source configurations diff --git a/internal/sources/types_test.go b/internal/sources/types_test.go index 9ed65ce..7cc2892 100644 --- a/internal/sources/types_test.go +++ b/internal/sources/types_test.go @@ -7,8 +7,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/stacklok/toolhive-registry-server/pkg/config" - "github.com/stacklok/toolhive-registry-server/pkg/registry" + "github.com/stacklok/toolhive-registry-server/internal/config" + "github.com/stacklok/toolhive-registry-server/internal/registry" ) func TestNewFetchResult(t *testing.T) { diff --git a/internal/sync/manager.go b/internal/sync/manager.go index 4e21c42..0a0f3b6 100644 --- a/internal/sync/manager.go +++ b/internal/sync/manager.go @@ -10,10 +10,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" - "github.com/stacklok/toolhive-registry-server/pkg/config" - "github.com/stacklok/toolhive-registry-server/pkg/filtering" - "github.com/stacklok/toolhive-registry-server/pkg/registry" - "github.com/stacklok/toolhive-registry-server/pkg/status" + "github.com/stacklok/toolhive-registry-server/internal/config" + "github.com/stacklok/toolhive-registry-server/internal/filtering" + "github.com/stacklok/toolhive-registry-server/internal/registry" + "github.com/stacklok/toolhive-registry-server/internal/sources" + "github.com/stacklok/toolhive-registry-server/internal/status" ) // Result contains the result of a successful sync operation @@ -127,8 +128,8 @@ type AutomaticSyncChecker interface { // DefaultSyncManager is the default implementation of Manager type DefaultSyncManager struct { - sourceHandlerFactory sources2.SourceHandlerFactory - storageManager sources2.StorageManager + sourceHandlerFactory sources.SourceHandlerFactory + storageManager sources.StorageManager filterService filtering.FilterService dataChangeDetector DataChangeDetector automaticSyncChecker AutomaticSyncChecker @@ -136,7 +137,7 @@ type DefaultSyncManager struct { // NewDefaultSyncManager creates a new DefaultSyncManager func NewDefaultSyncManager( - sourceHandlerFactory sources2.SourceHandlerFactory, storageManager sources2.StorageManager) *DefaultSyncManager { + sourceHandlerFactory sources.SourceHandlerFactory, storageManager sources.StorageManager) *DefaultSyncManager { return &DefaultSyncManager{ sourceHandlerFactory: sourceHandlerFactory, storageManager: storageManager, @@ -293,7 +294,7 @@ func (s *DefaultSyncManager) Delete(ctx context.Context, cfg *config.Config) err // fetchAndProcessRegistryData handles source handler creation, validation, fetch, and filtering func (s *DefaultSyncManager) fetchAndProcessRegistryData( ctx context.Context, - cfg *config.Config) (*sources2.FetchResult, *Error) { + cfg *config.Config) (*sources.FetchResult, *Error) { ctxLogger := log.FromContext(ctx) // Get source handler @@ -349,7 +350,7 @@ func (s *DefaultSyncManager) fetchAndProcessRegistryData( func (s *DefaultSyncManager) applyFilteringIfConfigured( ctx context.Context, cfg *config.Config, - fetchResult *sources2.FetchResult) *Error { + fetchResult *sources.FetchResult) *Error { ctxLogger := log.FromContext(ctx) if cfg.Filter != nil { @@ -412,7 +413,7 @@ func (s *DefaultSyncManager) applyFilteringIfConfigured( func (s *DefaultSyncManager) storeRegistryData( ctx context.Context, cfg *config.Config, - fetchResult *sources2.FetchResult) *Error { + fetchResult *sources.FetchResult) *Error { ctxLogger := log.FromContext(ctx) if err := s.storageManager.Store(ctx, cfg, fetchResult.Registry); err != nil { From 6f89f6ac89eab63bbee02a9126eae5a297fa5703 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Mon, 17 Nov 2025 19:01:40 +0100 Subject: [PATCH 3/4] Using toolhive UpstreamRegistry --- go.mod | 13 +- go.sum | 30 ++- internal/registry/server_registry.go | 145 ------------- internal/registry/server_registry_test.go | 205 +----------------- internal/registry/temporary.go | 54 +++++ internal/service/file_provider.go | 9 +- internal/service/file_provider_test.go | 10 +- internal/service/provider.go | 2 +- internal/sources/api_toolhive.go | 10 +- internal/sources/git_test.go | 26 +-- internal/sources/mocks/mock_source_handler.go | 6 +- .../sources/mocks/mock_storage_manager.go | 8 +- internal/sources/storage_manager.go | 19 +- internal/sources/storage_manager_test.go | 25 +-- internal/sources/types.go | 36 +-- internal/sources/types_test.go | 17 +- internal/sync/manager.go | 13 +- 17 files changed, 177 insertions(+), 451 deletions(-) delete mode 100644 internal/registry/server_registry.go create mode 100644 internal/registry/temporary.go diff --git a/go.mod b/go.mod index aa8d291..1d54dd6 100644 --- a/go.mod +++ b/go.mod @@ -13,8 +13,7 @@ require ( github.com/onsi/ginkgo/v2 v2.27.2 github.com/onsi/gomega v1.38.2 github.com/spf13/viper v1.21.0 - github.com/sqlc-dev/pqtype v0.3.0 - github.com/stacklok/toolhive v0.6.3-0.20251111071910-8739ec42d905 + github.com/stacklok/toolhive v0.6.6-0.20251117142941-d06cdcbf7cc4 github.com/stretchr/testify v1.11.1 github.com/swaggo/swag/v2 v2.0.0-rc4 github.com/testcontainers/testcontainers-go v0.40.0 @@ -138,19 +137,19 @@ require ( go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.43.0 // indirect + golang.org/x/crypto v0.44.0 // indirect golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.46.0 // indirect + golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sync v0.18.0 // indirect - golang.org/x/term v0.36.0 // indirect - golang.org/x/text v0.30.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect golang.org/x/tools v0.38.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/apimachinery v0.34.1 // indirect + k8s.io/apimachinery v0.34.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect diff --git a/go.sum b/go.sum index 1172636..d65de39 100644 --- a/go.sum +++ b/go.sum @@ -287,10 +287,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/sqlc-dev/pqtype v0.3.0 h1:b09TewZ3cSnO5+M1Kqq05y0+OjqIptxELaSayg7bmqk= -github.com/sqlc-dev/pqtype v0.3.0/go.mod h1:oyUjp5981ctiL9UYvj1bVvCKi8OXkCa0u645hce7CAs= -github.com/stacklok/toolhive v0.6.3-0.20251111071910-8739ec42d905 h1:QcZm42D+Fu+7sUmZPUDNInjyya9ic5Ccl6zKYb6z2i4= -github.com/stacklok/toolhive v0.6.3-0.20251111071910-8739ec42d905/go.mod h1:U6T5FqLzlwE+s4Ccp0lXvZbjX/SwG3YjVMYmSaSWPKU= +github.com/stacklok/toolhive v0.6.6-0.20251117142941-d06cdcbf7cc4 h1:GYLtT1j/P6B22/TMXSWlrjFB13u4tvirQRG3MGIRCMk= +github.com/stacklok/toolhive v0.6.6-0.20251117142941-d06cdcbf7cc4/go.mod h1:YbBeAB02mDMJkTiQICQTQi7iOC6pqhFivp1IoXWHjn4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= @@ -372,8 +370,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= @@ -383,8 +381,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -409,13 +407,13 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -453,10 +451,10 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= -k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= -k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= -k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= +k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= +k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= +k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= diff --git a/internal/registry/server_registry.go b/internal/registry/server_registry.go deleted file mode 100644 index 7853851..0000000 --- a/internal/registry/server_registry.go +++ /dev/null @@ -1,145 +0,0 @@ -// Package registry provides the unified internal registry format. -// -// ServerRegistry is the single source of truth for registry data, storing servers -// in upstream MCP ServerJSON format while maintaining ToolHive-compatible metadata -// fields for backward compatibility and versioning. -package registry - -import ( - "fmt" - "strings" - "time" - - upstreamv0 "github.com/modelcontextprotocol/registry/pkg/api/v0" - "github.com/stacklok/toolhive/pkg/registry/converters" - toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" -) - -// ServerRegistry is the unified internal registry format. -// It stores servers in upstream ServerJSON format while maintaining -// ToolHive-compatible metadata fields for backward compatibility. -type ServerRegistry struct { - // Version is the schema version (ToolHive compatibility) - Version string `json:"version"` - - // LastUpdated is the timestamp when registry was last updated (ToolHive compatibility) - LastUpdated string `json:"last_updated"` - - // Servers contains the server definitions in upstream MCP format - Servers []upstreamv0.ServerJSON `json:"servers"` -} - -// NewServerRegistryFromUpstream creates a ServerRegistry from upstream ServerJSON array. -// This is used when ingesting data from upstream MCP Registry API endpoints. -func NewServerRegistryFromUpstream(servers []upstreamv0.ServerJSON) *ServerRegistry { - return &ServerRegistry{ - Version: "1.0.0", - LastUpdated: time.Now().Format(time.RFC3339), - Servers: servers, - } -} - -// NewServerRegistryFromToolhive creates a ServerRegistry from ToolHive Registry. -// This converts ToolHive format to upstream ServerJSON using the converters package. -// Used when ingesting data from ToolHive-format sources (Git, File, API). -func NewServerRegistryFromToolhive(toolhiveReg *toolhivetypes.Registry) (*ServerRegistry, error) { - if toolhiveReg == nil { - return nil, fmt.Errorf("toolhive registry cannot be nil") - } - - servers := make([]upstreamv0.ServerJSON, 0, len(toolhiveReg.Servers)+len(toolhiveReg.RemoteServers)) - - // Convert container servers using converters package - for name, imgMeta := range toolhiveReg.Servers { - serverJSON, err := converters.ImageMetadataToServerJSON(name, imgMeta) - if err != nil { - return nil, fmt.Errorf("failed to convert server %s: %w", name, err) - } - servers = append(servers, *serverJSON) - } - - // Convert remote servers using converters package - for name, remoteMeta := range toolhiveReg.RemoteServers { - serverJSON, err := converters.RemoteServerMetadataToServerJSON(name, remoteMeta) - if err != nil { - return nil, fmt.Errorf("failed to convert remote server %s: %w", name, err) - } - servers = append(servers, *serverJSON) - } - - return &ServerRegistry{ - Version: toolhiveReg.Version, - LastUpdated: toolhiveReg.LastUpdated, - Servers: servers, - }, nil -} - -// ToToolhive converts ServerRegistry back to ToolHive Registry format. -// Used for backward compatibility with v0 API. -func (sr *ServerRegistry) ToToolhive() (*toolhivetypes.Registry, error) { - if sr == nil { - return nil, fmt.Errorf("server registry cannot be nil") - } - - toolhiveReg := &toolhivetypes.Registry{ - Version: sr.Version, - LastUpdated: sr.LastUpdated, - Servers: make(map[string]*toolhivetypes.ImageMetadata), - RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), - } - - for i := range sr.Servers { - serverJSON := &sr.Servers[i] - name := extractSimpleName(serverJSON.Name) - - // Detect server type by presence of packages vs remotes - if len(serverJSON.Packages) > 0 { - // Container server - imgMeta, err := converters.ServerJSONToImageMetadata(serverJSON) - if err != nil { - return nil, fmt.Errorf("failed to convert server %s: %w", serverJSON.Name, err) - } - toolhiveReg.Servers[name] = imgMeta - } else if len(serverJSON.Remotes) > 0 { - // Remote server - remoteMeta, err := converters.ServerJSONToRemoteServerMetadata(serverJSON) - if err != nil { - return nil, fmt.Errorf("failed to convert remote server %s: %w", serverJSON.Name, err) - } - toolhiveReg.RemoteServers[name] = remoteMeta - } - // Note: Servers with neither packages nor remotes are skipped - // This shouldn't happen with valid ServerJSON data - } - - return toolhiveReg, nil -} - -// GetServerByName retrieves a server by its name. -// Supports both reverse-DNS format (e.g., "io.github.user/server") and simple names (e.g., "server"). -func (sr *ServerRegistry) GetServerByName(name string) (*upstreamv0.ServerJSON, bool) { - if sr == nil { - return nil, false - } - - for i := range sr.Servers { - serverName := sr.Servers[i].Name - if serverName == name || extractSimpleName(serverName) == name { - return &sr.Servers[i], true - } - } - return nil, false -} - -// extractSimpleName extracts the simple server name from reverse-DNS format. -// Examples: -// - "io.github.user/server" -> "server" -// - "com.example/my-server" -> "my-server" -// - "simple-name" -> "simple-name" (no change if not reverse-DNS) -func extractSimpleName(reverseDNS string) string { - idx := strings.LastIndex(reverseDNS, "/") - if idx >= 0 && idx < len(reverseDNS)-1 { - return reverseDNS[idx+1:] - } - return reverseDNS -} diff --git a/internal/registry/server_registry_test.go b/internal/registry/server_registry_test.go index 774632b..bfe7c32 100644 --- a/internal/registry/server_registry_test.go +++ b/internal/registry/server_registry_test.go @@ -2,186 +2,27 @@ package registry import ( "testing" - "time" upstreamv0 "github.com/modelcontextprotocol/registry/pkg/api/v0" "github.com/modelcontextprotocol/registry/pkg/model" + "github.com/stacklok/toolhive/pkg/registry/converters" toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestNewServerRegistryFromToolhive(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - toolhiveReg *toolhivetypes.Registry - expectError bool - validate func(*testing.T, *ServerRegistry) - }{ - { - name: "successful conversion with container servers", - toolhiveReg: &toolhivetypes.Registry{ - Version: "1.0.0", - LastUpdated: "2024-01-01T00:00:00Z", - Servers: map[string]*toolhivetypes.ImageMetadata{ - "test-server": { - BaseServerMetadata: toolhivetypes.BaseServerMetadata{ - Name: "test-server", - Description: "A test server", - Tier: "Community", - Status: "Active", - Transport: "stdio", - Tools: []string{"test_tool"}, - }, - Image: "test/image:latest", - }, - }, - RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), - }, - expectError: false, - validate: func(t *testing.T, sr *ServerRegistry) { - t.Helper() - assert.Equal(t, "1.0.0", sr.Version) - assert.Equal(t, "2024-01-01T00:00:00Z", sr.LastUpdated) - assert.Len(t, sr.Servers, 1) - assert.Contains(t, sr.Servers[0].Name, "test-server") - assert.Equal(t, "A test server", sr.Servers[0].Description) - }, - }, - { - name: "successful conversion with remote servers", - toolhiveReg: &toolhivetypes.Registry{ - Version: "1.0.0", - LastUpdated: "2024-01-01T00:00:00Z", - Servers: make(map[string]*toolhivetypes.ImageMetadata), - RemoteServers: map[string]*toolhivetypes.RemoteServerMetadata{ - "remote-server": { - BaseServerMetadata: toolhivetypes.BaseServerMetadata{ - Name: "remote-server", - Description: "A remote server", - Tier: "Community", - Status: "Active", - Transport: "sse", - Tools: []string{"remote_tool"}, - }, - URL: "https://example.com", - }, - }, - }, - expectError: false, - validate: func(t *testing.T, sr *ServerRegistry) { - t.Helper() - assert.Len(t, sr.Servers, 1) - assert.Contains(t, sr.Servers[0].Name, "remote-server") - }, - }, - { - name: "empty registry", - toolhiveReg: &toolhivetypes.Registry{ - Version: "1.0.0", - LastUpdated: "2024-01-01T00:00:00Z", - Servers: make(map[string]*toolhivetypes.ImageMetadata), - RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), - }, - expectError: false, - validate: func(t *testing.T, sr *ServerRegistry) { - t.Helper() - assert.Empty(t, sr.Servers) - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - result, err := NewServerRegistryFromToolhive(tt.toolhiveReg) - - if tt.expectError { - assert.Error(t, err) - assert.Nil(t, result) - } else { - assert.NoError(t, err) - assert.NotNil(t, result) - if tt.validate != nil { - tt.validate(t, result) - } - } - }) - } -} - -func TestNewServerRegistryFromUpstream(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - servers []upstreamv0.ServerJSON - validate func(*testing.T, *ServerRegistry) - }{ - { - name: "create from upstream servers", - servers: []upstreamv0.ServerJSON{ - { - Schema: "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json", - Name: "io.test/server1", - Description: "Test server 1", - Version: "1.0.0", - Packages: []model.Package{ - { - RegistryType: "oci", - Identifier: "test/image:latest", - Transport: model.Transport{Type: "stdio"}, - }, - }, - }, - }, - validate: func(t *testing.T, sr *ServerRegistry) { - t.Helper() - assert.Equal(t, "1.0.0", sr.Version) - assert.NotEmpty(t, sr.LastUpdated) - assert.Len(t, sr.Servers, 1) - assert.Equal(t, "io.test/server1", sr.Servers[0].Name) - }, - }, - { - name: "create from empty slice", - servers: []upstreamv0.ServerJSON{}, - validate: func(t *testing.T, sr *ServerRegistry) { - t.Helper() - assert.Empty(t, sr.Servers) - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - result := NewServerRegistryFromUpstream(tt.servers) - - assert.NotNil(t, result) - if tt.validate != nil { - tt.validate(t, result) - } - }) - } -} - -func TestServerRegistry_ToToolhive(t *testing.T) { +func TestUpstreamRegistry_ToToolhive(t *testing.T) { t.Parallel() tests := []struct { name string - serverReg *ServerRegistry + serverReg *toolhivetypes.UpstreamRegistry expectError bool validateFunc func(*testing.T, *toolhivetypes.Registry) }{ { name: "convert to toolhive with servers", - serverReg: &ServerRegistry{ + serverReg: &toolhivetypes.UpstreamRegistry{ Version: "1.0.0", LastUpdated: "2024-01-01T00:00:00Z", Servers: []upstreamv0.ServerJSON{ @@ -210,7 +51,7 @@ func TestServerRegistry_ToToolhive(t *testing.T) { }, { name: "convert empty registry", - serverReg: &ServerRegistry{ + serverReg: &toolhivetypes.UpstreamRegistry{ Version: "1.0.0", LastUpdated: "2024-01-01T00:00:00Z", Servers: []upstreamv0.ServerJSON{}, @@ -228,8 +69,8 @@ func TestServerRegistry_ToToolhive(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - result, err := tt.serverReg.ToToolhive() - + result, err := ToToolhive(tt.serverReg) + require.NoError(t, err) if tt.expectError { assert.Error(t, err) assert.Nil(t, result) @@ -244,7 +85,7 @@ func TestServerRegistry_ToToolhive(t *testing.T) { } } -func TestServerRegistry_RoundTripConversion(t *testing.T) { +func TestUpstreamRegistry_RoundTripConversion(t *testing.T) { t.Parallel() originalToolhive := &toolhivetypes.Registry{ @@ -266,13 +107,13 @@ func TestServerRegistry_RoundTripConversion(t *testing.T) { RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), } - // Convert to ServerRegistry - serverReg, err := NewServerRegistryFromToolhive(originalToolhive) + // Convert to UpstreamRegistry + serverReg, err := converters.NewUpstreamRegistryFromToolhiveRegistry(originalToolhive) require.NoError(t, err) require.NotNil(t, serverReg) // Convert back to ToolHive - convertedBack, err := serverReg.ToToolhive() + convertedBack, err := ToToolhive(serverReg) require.NoError(t, err) require.NotNil(t, convertedBack) @@ -281,27 +122,3 @@ func TestServerRegistry_RoundTripConversion(t *testing.T) { assert.Equal(t, originalToolhive.LastUpdated, convertedBack.LastUpdated) assert.Len(t, convertedBack.Servers, 1) } - -func TestNewServerRegistryFromUpstream_DefaultValues(t *testing.T) { - t.Parallel() - - servers := []upstreamv0.ServerJSON{ - { - Schema: "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json", - Name: "io.test/server1", - Description: "Test server", - Version: "1.0.0", - }, - } - - result := NewServerRegistryFromUpstream(servers) - - // Verify defaults - assert.Equal(t, "1.0.0", result.Version) - assert.NotEmpty(t, result.LastUpdated) - - // Verify timestamp is recent (within last minute) - parsedTime, err := time.Parse(time.RFC3339, result.LastUpdated) - require.NoError(t, err) - assert.WithinDuration(t, time.Now(), parsedTime, time.Minute) -} diff --git a/internal/registry/temporary.go b/internal/registry/temporary.go new file mode 100644 index 0000000..f81dae0 --- /dev/null +++ b/internal/registry/temporary.go @@ -0,0 +1,54 @@ +// Package registry provides the unified internal registry format. +// +// UpstreamRegistry is the single source of truth for registry data, storing servers +// in upstream MCP ServerJSON format while maintaining ToolHive-compatible metadata +// fields for backward compatibility and versioning. +package registry + +import ( + "fmt" + + "github.com/stacklok/toolhive/pkg/registry/converters" + toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" +) + +// ToToolhive converts UpstreamRegistry back to ToolHive Registry format. +// Used for backward compatibility with v0 API. +func ToToolhive(upstreamRegistry *toolhivetypes.UpstreamRegistry) (*toolhivetypes.Registry, error) { + if upstreamRegistry == nil { + return nil, fmt.Errorf("upstream registry cannot be nil") + } + + toolhiveReg := &toolhivetypes.Registry{ + Version: upstreamRegistry.Version, + LastUpdated: upstreamRegistry.LastUpdated, + Servers: make(map[string]*toolhivetypes.ImageMetadata), + RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), + } + + for i := range upstreamRegistry.Servers { + serverJSON := &upstreamRegistry.Servers[i] + name := serverJSON.Name + + // Detect server type by presence of packages vs remotes + if len(serverJSON.Packages) > 0 { + // Container server + imgMeta, err := converters.ServerJSONToImageMetadata(serverJSON) + if err != nil { + return nil, fmt.Errorf("failed to convert server %s: %w", serverJSON.Name, err) + } + toolhiveReg.Servers[name] = imgMeta + } else if len(serverJSON.Remotes) > 0 { + // Remote server + remoteMeta, err := converters.ServerJSONToRemoteServerMetadata(serverJSON) + if err != nil { + return nil, fmt.Errorf("failed to convert remote server %s: %w", serverJSON.Name, err) + } + toolhiveReg.RemoteServers[name] = remoteMeta + } + // Note: Servers with neither packages nor remotes are skipped + // This shouldn't happen with valid ServerJSON data + } + + return toolhiveReg, nil +} diff --git a/internal/service/file_provider.go b/internal/service/file_provider.go index f4a584b..7c8b288 100644 --- a/internal/service/file_provider.go +++ b/internal/service/file_provider.go @@ -8,6 +8,7 @@ import ( toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stacklok/toolhive-registry-server/internal/config" + "github.com/stacklok/toolhive-registry-server/internal/registry" "github.com/stacklok/toolhive-registry-server/internal/sources" ) @@ -35,18 +36,18 @@ func NewFileRegistryDataProvider(storageManager sources.StorageManager, cfg *con // It delegates to the StorageManager to retrieve and parse registry data. // This eliminates code duplication and provides a single source of truth for file operations. // -// NOTE: In PR 1, StorageManager returns ServerRegistry but RegistryDataProvider interface +// NOTE: In PR 1, StorageManager returns UpstreamRegistry but RegistryDataProvider interface // still expects toolhive Registry. This method converts at the boundary to maintain // backward compatibility until PR 2. func (p *FileRegistryDataProvider) GetRegistryData(ctx context.Context) (*toolhivetypes.Registry, error) { - // Get ServerRegistry from storage manager (new format) + // Get UpstreamRegistry from storage manager (new format) serverReg, err := p.storageManager.Get(ctx, p.config) if err != nil { return nil, fmt.Errorf("failed to get registry data: %w", err) } - // Convert ServerRegistry → ToolHive Registry using ToToolhive() method - toolhiveReg, err := serverReg.ToToolhive() + // Convert UpstreamRegistry → ToolHive Registry using ToToolhive() method + toolhiveReg, err := registry.ToToolhive(serverReg) if err != nil { return nil, fmt.Errorf("failed to convert to toolhive format: %w", err) } diff --git a/internal/service/file_provider_test.go b/internal/service/file_provider_test.go index c930883..df5ea2a 100644 --- a/internal/service/file_provider_test.go +++ b/internal/service/file_provider_test.go @@ -5,13 +5,13 @@ import ( "errors" "testing" + "github.com/stacklok/toolhive/pkg/registry/converters" toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "github.com/stacklok/toolhive-registry-server/internal/config" - "github.com/stacklok/toolhive-registry-server/internal/registry" sourcesmocks "github.com/stacklok/toolhive-registry-server/internal/sources/mocks" ) @@ -43,8 +43,8 @@ func TestFileRegistryDataProvider_GetRegistryData(t *testing.T) { }, RemoteServers: map[string]*toolhivetypes.RemoteServerMetadata{}, } - // Convert to ServerRegistry - expectedRegistry, _ := registry.NewServerRegistryFromToolhive(toolhiveRegistry) + // Convert to UpstreamRegistry + expectedRegistry, _ := converters.NewUpstreamRegistryFromToolhiveRegistry(toolhiveRegistry) m.EXPECT(). Get(gomock.Any(), gomock.Any()). Return(expectedRegistry, nil) @@ -66,8 +66,8 @@ func TestFileRegistryDataProvider_GetRegistryData(t *testing.T) { Servers: map[string]*toolhivetypes.ImageMetadata{}, RemoteServers: map[string]*toolhivetypes.RemoteServerMetadata{}, } - // Convert to ServerRegistry - expectedRegistry, _ := registry.NewServerRegistryFromToolhive(toolhiveRegistry) + // Convert to UpstreamRegistry + expectedRegistry, _ := converters.NewUpstreamRegistryFromToolhiveRegistry(toolhiveRegistry) m.EXPECT(). Get(gomock.Any(), gomock.Any()). Return(expectedRegistry, nil) diff --git a/internal/service/provider.go b/internal/service/provider.go index 2aad0db..6f12b5b 100644 --- a/internal/service/provider.go +++ b/internal/service/provider.go @@ -16,7 +16,7 @@ type RegistryDataProvider interface { // GetRegistryData fetches the current registry data. // Returns the registry data and any error encountered. // NOTE: In PR 1, this still returns ToolHive Registry for backward compatibility. - // PR 2 will change this to return ServerRegistry. + // PR 2 will change this to return UpstreamRegistry. GetRegistryData(ctx context.Context) (*toolhivetypes.Registry, error) // GetSource returns a descriptive string about where the registry data comes from. diff --git a/internal/sources/api_toolhive.go b/internal/sources/api_toolhive.go index 8fca089..1ec9bad 100644 --- a/internal/sources/api_toolhive.go +++ b/internal/sources/api_toolhive.go @@ -10,12 +10,12 @@ import ( "time" "github.com/go-logr/logr" + "github.com/stacklok/toolhive/pkg/registry/converters" toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "sigs.k8s.io/controller-runtime/pkg/log" "github.com/stacklok/toolhive-registry-server/internal/config" "github.com/stacklok/toolhive-registry-server/internal/httpclient" - "github.com/stacklok/toolhive-registry-server/internal/registry" ) // ToolHiveAPIHandler handles registry data from ToolHive Registry API endpoints @@ -101,17 +101,17 @@ func (h *ToolHiveAPIHandler) FetchRegistry(ctx context.Context, registryConfig * return nil, fmt.Errorf("failed to convert to ToolHive format: %w", err) } - // Convert ToolHive Registry to ServerRegistry - serverRegistry, err := registry.NewServerRegistryFromToolhive(toolhiveRegistry) + // Convert ToolHive Registry to UpstreamRegistry + UpstreamRegistry, err := converters.NewUpstreamRegistryFromToolhiveRegistry(toolhiveRegistry) if err != nil { - return nil, fmt.Errorf("failed to convert to ServerRegistry: %w", err) + return nil, fmt.Errorf("failed to convert to UpstreamRegistry: %w", err) } // Calculate hash of the raw data for change detection hash := fmt.Sprintf("%x", sha256.Sum256(data)) // Create and return fetch result - return NewFetchResult(serverRegistry, hash, config.SourceFormatToolHive), nil + return NewFetchResult(UpstreamRegistry, hash, config.SourceFormatToolHive), nil } // CurrentHash returns the current hash of the API response diff --git a/internal/sources/git_test.go b/internal/sources/git_test.go index 5c48e38..a923cea 100644 --- a/internal/sources/git_test.go +++ b/internal/sources/git_test.go @@ -7,6 +7,7 @@ import ( "fmt" "testing" + "github.com/stacklok/toolhive/pkg/registry/converters" toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -14,7 +15,6 @@ import ( "github.com/stacklok/toolhive-registry-server/internal/config" "github.com/stacklok/toolhive-registry-server/internal/git" - "github.com/stacklok/toolhive-registry-server/internal/registry" ) const ( @@ -66,12 +66,12 @@ type MockSourceDataValidator struct { mock.Mock } -func (m *MockSourceDataValidator) ValidateData(data []byte, format string) (*registry.ServerRegistry, error) { +func (m *MockSourceDataValidator) ValidateData(data []byte, format string) (*toolhivetypes.UpstreamRegistry, error) { args := m.Called(data, format) if args.Get(0) == nil { return nil, args.Error(1) } - return args.Get(0).(*registry.ServerRegistry), args.Error(1) + return args.Get(0).(*toolhivetypes.UpstreamRegistry), args.Error(1) } func TestNewGitSourceHandler(t *testing.T) { @@ -288,8 +288,8 @@ func TestGitSourceHandler_FetchRegistry(t *testing.T) { RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), } - // Convert to ServerRegistry - serverRegistry, _ := registry.NewServerRegistryFromToolhive(testRegistry) + // Convert to UpstreamRegistry + UpstreamRegistry, _ := converters.NewUpstreamRegistryFromToolhiveRegistry(testRegistry) gitClient.On("Clone", mock.Anything, mock.MatchedBy(func(config *git.CloneConfig) bool { return config.URL == testGitRepoURL && config.Branch == testBranch @@ -298,7 +298,7 @@ func TestGitSourceHandler_FetchRegistry(t *testing.T) { gitClient.On("GetFileContent", repoInfo, DefaultRegistryDataFile).Return(testData, nil) gitClient.On("Cleanup", repoInfo).Return(nil) - validator.On("ValidateData", testData, config.SourceFormatToolHive).Return(serverRegistry, nil) + validator.On("ValidateData", testData, config.SourceFormatToolHive).Return(UpstreamRegistry, nil) }, expectError: false, }, @@ -326,8 +326,8 @@ func TestGitSourceHandler_FetchRegistry(t *testing.T) { RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), } - // Convert to ServerRegistry - serverRegistry, _ := registry.NewServerRegistryFromToolhive(testRegistry) + // Convert to UpstreamRegistry + UpstreamRegistry, _ := converters.NewUpstreamRegistryFromToolhiveRegistry(testRegistry) gitClient.On("Clone", mock.Anything, mock.MatchedBy(func(config *git.CloneConfig) bool { return config.URL == testGitRepoURL && config.Tag == testTag @@ -336,7 +336,7 @@ func TestGitSourceHandler_FetchRegistry(t *testing.T) { gitClient.On("GetFileContent", repoInfo, testFilePath).Return(testData, nil) gitClient.On("Cleanup", repoInfo).Return(nil) - validator.On("ValidateData", testData, config.SourceFormatToolHive).Return(serverRegistry, nil) + validator.On("ValidateData", testData, config.SourceFormatToolHive).Return(UpstreamRegistry, nil) }, expectError: false, }, @@ -421,7 +421,7 @@ func TestGitSourceHandler_FetchRegistry(t *testing.T) { gitClient.On("GetFileContent", repoInfo, DefaultRegistryDataFile).Return(testData, nil) gitClient.On("Cleanup", repoInfo).Return(nil) - validator.On("ValidateData", testData, config.SourceFormatToolHive).Return((*registry.ServerRegistry)(nil), errors.New("invalid data")) + validator.On("ValidateData", testData, config.SourceFormatToolHive).Return((*toolhivetypes.UpstreamRegistry)(nil), errors.New("invalid data")) }, expectError: true, errorContains: "registry data validation failed", @@ -649,15 +649,15 @@ func TestGitSourceHandler_CleanupFailure(t *testing.T) { RemoteServers: make(map[string]*toolhivetypes.RemoteServerMetadata), } - // Convert to ServerRegistry - serverRegistry, err := registry.NewServerRegistryFromToolhive(testRegistry) + // Convert to UpstreamRegistry + UpstreamRegistry, err := converters.NewUpstreamRegistryFromToolhiveRegistry(testRegistry) require.NoError(t, err) mockGitClient.On("Clone", mock.Anything, mock.Anything).Return(repoInfo, nil) mockGitClient.On("GetFileContent", repoInfo, DefaultRegistryDataFile).Return(testData, nil) mockGitClient.On("Cleanup", repoInfo).Return(errors.New("cleanup failed")) // Cleanup fails - mockValidator.On("ValidateData", testData, config.SourceFormatToolHive).Return(serverRegistry, nil) + mockValidator.On("ValidateData", testData, config.SourceFormatToolHive).Return(UpstreamRegistry, nil) handler := &GitSourceHandler{ gitClient: mockGitClient, diff --git a/internal/sources/mocks/mock_source_handler.go b/internal/sources/mocks/mock_source_handler.go index a6dc2a5..7f3e6ba 100644 --- a/internal/sources/mocks/mock_source_handler.go +++ b/internal/sources/mocks/mock_source_handler.go @@ -14,8 +14,8 @@ import ( reflect "reflect" config "github.com/stacklok/toolhive-registry-server/internal/config" - registry "github.com/stacklok/toolhive-registry-server/internal/registry" sources "github.com/stacklok/toolhive-registry-server/internal/sources" + types "github.com/stacklok/toolhive/pkg/registry/types" gomock "go.uber.org/mock/gomock" ) @@ -44,10 +44,10 @@ func (m *MockSourceDataValidator) EXPECT() *MockSourceDataValidatorMockRecorder } // ValidateData mocks base method. -func (m *MockSourceDataValidator) ValidateData(data []byte, format string) (*registry.ServerRegistry, error) { +func (m *MockSourceDataValidator) ValidateData(data []byte, format string) (*types.UpstreamRegistry, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ValidateData", data, format) - ret0, _ := ret[0].(*registry.ServerRegistry) + ret0, _ := ret[0].(*types.UpstreamRegistry) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/internal/sources/mocks/mock_storage_manager.go b/internal/sources/mocks/mock_storage_manager.go index a0e820a..8c253f5 100644 --- a/internal/sources/mocks/mock_storage_manager.go +++ b/internal/sources/mocks/mock_storage_manager.go @@ -14,7 +14,7 @@ import ( reflect "reflect" config "github.com/stacklok/toolhive-registry-server/internal/config" - registry "github.com/stacklok/toolhive-registry-server/internal/registry" + types "github.com/stacklok/toolhive/pkg/registry/types" gomock "go.uber.org/mock/gomock" ) @@ -57,10 +57,10 @@ func (mr *MockStorageManagerMockRecorder) Delete(ctx, cfg any) *gomock.Call { } // Get mocks base method. -func (m *MockStorageManager) Get(ctx context.Context, cfg *config.Config) (*registry.ServerRegistry, error) { +func (m *MockStorageManager) Get(ctx context.Context, cfg *config.Config) (*types.UpstreamRegistry, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", ctx, cfg) - ret0, _ := ret[0].(*registry.ServerRegistry) + ret0, _ := ret[0].(*types.UpstreamRegistry) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -72,7 +72,7 @@ func (mr *MockStorageManagerMockRecorder) Get(ctx, cfg any) *gomock.Call { } // Store mocks base method. -func (m *MockStorageManager) Store(ctx context.Context, cfg *config.Config, reg *registry.ServerRegistry) error { +func (m *MockStorageManager) Store(ctx context.Context, cfg *config.Config, reg *types.UpstreamRegistry) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Store", ctx, cfg, reg) ret0, _ := ret[0].(error) diff --git a/internal/sources/storage_manager.go b/internal/sources/storage_manager.go index 4a3fe38..ab5ee53 100644 --- a/internal/sources/storage_manager.go +++ b/internal/sources/storage_manager.go @@ -7,8 +7,9 @@ import ( "os" "path/filepath" + toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" + "github.com/stacklok/toolhive-registry-server/internal/config" - "github.com/stacklok/toolhive-registry-server/internal/registry" ) const ( @@ -20,11 +21,11 @@ const ( // StorageManager defines the interface for registry data persistence type StorageManager interface { - // Store saves a ServerRegistry instance to persistent storage - Store(ctx context.Context, cfg *config.Config, reg *registry.ServerRegistry) error + // Store saves a UpstreamRegistry instance to persistent storage + Store(ctx context.Context, cfg *config.Config, reg *toolhivetypes.UpstreamRegistry) error // Get retrieves and parses registry data from persistent storage - Get(ctx context.Context, cfg *config.Config) (*registry.ServerRegistry, error) + Get(ctx context.Context, cfg *config.Config) (*toolhivetypes.UpstreamRegistry, error) // Delete removes registry data from persistent storage Delete(ctx context.Context, cfg *config.Config) error @@ -43,7 +44,7 @@ func NewFileStorageManager(basePath string) StorageManager { } // Store saves the registry data to a JSON file -func (f *FileStorageManager) Store(_ context.Context, _ *config.Config, reg *registry.ServerRegistry) error { +func (f *FileStorageManager) Store(_ context.Context, _ *config.Config, reg *toolhivetypes.UpstreamRegistry) error { // Create base directory if it doesn't exist if err := os.MkdirAll(f.basePath, 0750); err != nil { return fmt.Errorf("failed to create storage directory: %w", err) @@ -51,7 +52,7 @@ func (f *FileStorageManager) Store(_ context.Context, _ *config.Config, reg *reg filePath := filepath.Join(f.basePath, RegistryFileName) - // Marshal ServerRegistry to JSON with pretty printing for readability + // Marshal UpstreamRegistry to JSON with pretty printing for readability data, err := json.MarshalIndent(reg, "", " ") if err != nil { return fmt.Errorf("failed to marshal registry data: %w", err) @@ -74,7 +75,7 @@ func (f *FileStorageManager) Store(_ context.Context, _ *config.Config, reg *reg } // Get retrieves and parses registry data from the JSON file -func (f *FileStorageManager) Get(_ context.Context, _ *config.Config) (*registry.ServerRegistry, error) { +func (f *FileStorageManager) Get(_ context.Context, _ *config.Config) (*toolhivetypes.UpstreamRegistry, error) { filePath := filepath.Join(f.basePath, RegistryFileName) // Read file @@ -87,8 +88,8 @@ func (f *FileStorageManager) Get(_ context.Context, _ *config.Config) (*registry return nil, fmt.Errorf("failed to read registry file: %w", err) } - // Unmarshal JSON to ServerRegistry - var reg registry.ServerRegistry + // Unmarshal JSON to UpstreamRegistry + var reg toolhivetypes.UpstreamRegistry if err := json.Unmarshal(data, ®); err != nil { return nil, fmt.Errorf("failed to unmarshal registry data: %w", err) } diff --git a/internal/sources/storage_manager_test.go b/internal/sources/storage_manager_test.go index f4099aa..df06e52 100644 --- a/internal/sources/storage_manager_test.go +++ b/internal/sources/storage_manager_test.go @@ -6,10 +6,9 @@ import ( "path/filepath" "testing" + "github.com/stacklok/toolhive/pkg/registry/converters" toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stretchr/testify/require" - - "github.com/stacklok/toolhive-registry-server/internal/registry" ) func TestFileStorageManager_StoreAndGet(t *testing.T) { @@ -28,13 +27,13 @@ func TestFileStorageManager_StoreAndGet(t *testing.T) { Servers: make(map[string]*toolhivetypes.ImageMetadata), } - // Convert to ServerRegistry - serverRegistry, err := registry.NewServerRegistryFromToolhive(testRegistry) + // Convert to UpstreamRegistry + UpstreamRegistry, err := converters.NewUpstreamRegistryFromToolhiveRegistry(testRegistry) require.NoError(t, err) // Store the registry ctx := context.Background() - err = manager.Store(ctx, nil, serverRegistry) + err = manager.Store(ctx, nil, UpstreamRegistry) require.NoError(t, err) // Verify file was created @@ -46,8 +45,8 @@ func TestFileStorageManager_StoreAndGet(t *testing.T) { retrieved, err := manager.Get(ctx, nil) require.NoError(t, err) require.NotNil(t, retrieved) - require.Equal(t, serverRegistry.Version, retrieved.Version) - require.Equal(t, serverRegistry.LastUpdated, retrieved.LastUpdated) + require.Equal(t, UpstreamRegistry.Version, retrieved.Version) + require.Equal(t, UpstreamRegistry.LastUpdated, retrieved.LastUpdated) } func TestFileStorageManager_Delete(t *testing.T) { @@ -64,12 +63,12 @@ func TestFileStorageManager_Delete(t *testing.T) { Version: "1.0.0", } - // Convert to ServerRegistry - serverRegistry, err := registry.NewServerRegistryFromToolhive(testRegistry) + // Convert to UpstreamRegistry + UpstreamRegistry, err := converters.NewUpstreamRegistryFromToolhiveRegistry(testRegistry) require.NoError(t, err) ctx := context.Background() - err = manager.Store(ctx, nil, serverRegistry) + err = manager.Store(ctx, nil, UpstreamRegistry) require.NoError(t, err) // Delete the registry @@ -106,12 +105,12 @@ func TestFileStorageManager_Delete_PermissionDenied(t *testing.T) { // Create and store a file testRegistry := &toolhivetypes.Registry{Version: "1.0.0"} - // Convert to ServerRegistry - serverRegistry, err := registry.NewServerRegistryFromToolhive(testRegistry) + // Convert to UpstreamRegistry + UpstreamRegistry, err := converters.NewUpstreamRegistryFromToolhiveRegistry(testRegistry) require.NoError(t, err) ctx := context.Background() - err = manager.Store(ctx, nil, serverRegistry) + err = manager.Store(ctx, nil, UpstreamRegistry) require.NoError(t, err) // Make directory read-only to prevent deletion diff --git a/internal/sources/types.go b/internal/sources/types.go index 47a243e..727b447 100644 --- a/internal/sources/types.go +++ b/internal/sources/types.go @@ -7,16 +7,16 @@ import ( upstreamv0 "github.com/modelcontextprotocol/registry/pkg/api/v0" toolhiveregistry "github.com/stacklok/toolhive/pkg/registry" + "github.com/stacklok/toolhive/pkg/registry/converters" toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stacklok/toolhive-registry-server/internal/config" - "github.com/stacklok/toolhive-registry-server/internal/registry" ) // SourceDataValidator is an interface for validating registry source configurations type SourceDataValidator interface { - // ValidateData validates raw data and returns a parsed ServerRegistry - ValidateData(data []byte, format string) (*registry.ServerRegistry, error) + // ValidateData validates raw data and returns a parsed UpstreamRegistry + ValidateData(data []byte, format string) (*toolhivetypes.UpstreamRegistry, error) } //go:generate mockgen -destination=mocks/mock_source_handler.go -package=mocks -source=types.go SourceHandler,SourceHandlerFactory @@ -35,8 +35,8 @@ type SourceHandler interface { // FetchResult contains the result of a fetch operation type FetchResult struct { - // Registry is the parsed registry data in unified ServerRegistry format - Registry *registry.ServerRegistry + // Registry is the parsed registry data in unified UpstreamRegistry format + Registry *toolhivetypes.UpstreamRegistry // Hash is the SHA256 hash of the serialized data for change detection Hash string @@ -48,9 +48,9 @@ type FetchResult struct { Format string } -// NewFetchResult creates a new FetchResult from a ServerRegistry instance and pre-calculated hash +// NewFetchResult creates a new FetchResult from a UpstreamRegistry instance and pre-calculated hash // The hash should be calculated by the source handler to ensure consistency with CurrentHash -func NewFetchResult(reg *registry.ServerRegistry, hash string, format string) *FetchResult { +func NewFetchResult(reg *toolhivetypes.UpstreamRegistry, hash string, format string) *FetchResult { serverCount := 0 if reg != nil { serverCount = len(reg.Servers) @@ -78,8 +78,8 @@ func NewSourceDataValidator() SourceDataValidator { return &DefaultSourceDataValidator{} } -// ValidateData validates raw data and returns a parsed ServerRegistry -func (*DefaultSourceDataValidator) ValidateData(data []byte, format string) (*registry.ServerRegistry, error) { +// ValidateData validates raw data and returns a parsed UpstreamRegistry +func (*DefaultSourceDataValidator) ValidateData(data []byte, format string) (*toolhivetypes.UpstreamRegistry, error) { if len(data) == 0 { return nil, fmt.Errorf("data cannot be empty") } @@ -94,8 +94,8 @@ func (*DefaultSourceDataValidator) ValidateData(data []byte, format string) (*re } } -// validateToolhiveFormatAndParse validates data against ToolHive registry format and returns parsed ServerRegistry -func validateToolhiveFormatAndParse(data []byte) (*registry.ServerRegistry, error) { +// validateToolhiveFormatAndParse validates data against ToolHive registry format and returns parsed UpstreamRegistry +func validateToolhiveFormatAndParse(data []byte) (*toolhivetypes.UpstreamRegistry, error) { // Use the existing schema validation from toolhive package if err := toolhiveregistry.ValidateRegistrySchema(data); err != nil { return nil, err @@ -107,17 +107,17 @@ func validateToolhiveFormatAndParse(data []byte) (*registry.ServerRegistry, erro return nil, fmt.Errorf("failed to parse ToolHive registry format: %w", err) } - // Convert to ServerRegistry using constructor - serverReg, err := registry.NewServerRegistryFromToolhive(&toolhiveReg) + // Convert to UpstreamRegistry using constructor + serverReg, err := converters.NewUpstreamRegistryFromToolhiveRegistry(&toolhiveReg) if err != nil { - return nil, fmt.Errorf("failed to convert to ServerRegistry: %w", err) + return nil, fmt.Errorf("failed to convert to UpstreamRegistry: %w", err) } return serverReg, nil } -// validateUpstreamFormatAndParse validates data against upstream registry format and returns ServerRegistry -func validateUpstreamFormatAndParse(data []byte) (*registry.ServerRegistry, error) { +// validateUpstreamFormatAndParse validates data against upstream registry format and returns UpstreamRegistry +func validateUpstreamFormatAndParse(data []byte) (*toolhivetypes.UpstreamRegistry, error) { // Parse as upstream ServerResponse array to validate structure var responses []upstreamv0.ServerResponse if err := json.Unmarshal(data, &responses); err != nil { @@ -141,6 +141,6 @@ func validateUpstreamFormatAndParse(data []byte) (*registry.ServerRegistry, erro } } - // Wrap in ServerRegistry using constructor - return registry.NewServerRegistryFromUpstream(servers), nil + // Wrap in UpstreamRegistry using constructor + return converters.NewUpstreamRegistryFromUpstreamServers(servers), nil } diff --git a/internal/sources/types_test.go b/internal/sources/types_test.go index 7cc2892..96a4ff1 100644 --- a/internal/sources/types_test.go +++ b/internal/sources/types_test.go @@ -3,6 +3,7 @@ package sources import ( "testing" + "github.com/stacklok/toolhive/pkg/registry/converters" toolhivetypes "github.com/stacklok/toolhive/pkg/registry/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -61,8 +62,8 @@ func TestNewFetchResult(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - // Convert ToolHive Registry to ServerRegistry - serverReg, err := registry.NewServerRegistryFromToolhive(tt.registryData) + // Convert ToolHive Registry to UpstreamRegistry + serverReg, err := converters.NewUpstreamRegistryFromToolhiveRegistry(tt.registryData) require.NoError(t, err, "Failed to convert toolhive registry to server registry") result := NewFetchResult(serverReg, tt.hash, tt.format) @@ -73,7 +74,7 @@ func TestNewFetchResult(t *testing.T) { assert.Equal(t, tt.format, result.Format) // Verify registry by converting back - convertedBack, err := result.Registry.ToToolhive() + convertedBack, err := registry.ToToolhive(result.Registry) require.NoError(t, err, "Failed to convert server registry back to toolhive") assert.Equal(t, len(tt.registryData.Servers), len(convertedBack.Servers)) assert.Equal(t, len(tt.registryData.RemoteServers), len(convertedBack.RemoteServers)) @@ -98,8 +99,8 @@ func TestFetchResultHashConsistency(t *testing.T) { hash := "consistent-hash-value" format := config.SourceFormatToolHive - // Convert to ServerRegistry - serverReg, err := registry.NewServerRegistryFromToolhive(registryData) + // Convert to UpstreamRegistry + serverReg, err := converters.NewUpstreamRegistryFromToolhiveRegistry(registryData) require.NoError(t, err) result1 := NewFetchResult(serverReg, hash, format) @@ -135,10 +136,10 @@ func TestFetchResultHashDifference(t *testing.T) { hash2 := "hash-for-data2" format := config.SourceFormatToolHive - // Convert to ServerRegistry - serverReg1, err := registry.NewServerRegistryFromToolhive(registryData1) + // Convert to UpstreamRegistry + serverReg1, err := converters.NewUpstreamRegistryFromToolhiveRegistry(registryData1) require.NoError(t, err) - serverReg2, err := registry.NewServerRegistryFromToolhive(registryData2) + serverReg2, err := converters.NewUpstreamRegistryFromToolhiveRegistry(registryData2) require.NoError(t, err) result1 := NewFetchResult(serverReg1, hash1, format) diff --git a/internal/sync/manager.go b/internal/sync/manager.go index 0a0f3b6..4328304 100644 --- a/internal/sync/manager.go +++ b/internal/sync/manager.go @@ -8,6 +8,7 @@ import ( "fmt" "time" + "github.com/stacklok/toolhive/pkg/registry/converters" "sigs.k8s.io/controller-runtime/pkg/log" "github.com/stacklok/toolhive-registry-server/internal/config" @@ -358,8 +359,8 @@ func (s *DefaultSyncManager) applyFilteringIfConfigured( "hasNameFilters", cfg.Filter.Names != nil, "hasTagFilters", cfg.Filter.Tags != nil) - // Convert ServerRegistry to ToolHive format for filtering - toolhiveReg, err := fetchResult.Registry.ToToolhive() + // Convert UpstreamRegistry to ToolHive format for filtering + toolhiveReg, err := registry.ToToolhive(fetchResult.Registry) if err != nil { ctxLogger.Error(err, "Failed to convert to ToolHive format for filtering") return &Error{ @@ -381,13 +382,13 @@ func (s *DefaultSyncManager) applyFilteringIfConfigured( } } - // Convert filtered ToolHive registry back to ServerRegistry - filteredServerReg, err := registry.NewServerRegistryFromToolhive(filteredToolhiveReg) + // Convert filtered ToolHive registry back to UpstreamRegistry + filteredServerReg, err := converters.NewUpstreamRegistryFromToolhiveRegistry(filteredToolhiveReg) if err != nil { - ctxLogger.Error(err, "Failed to convert filtered registry to ServerRegistry") + ctxLogger.Error(err, "Failed to convert filtered registry to UpstreamRegistry") return &Error{ Err: err, - Message: fmt.Sprintf("Conversion to ServerRegistry failed: %v", err), + Message: fmt.Sprintf("Conversion to UpstreamRegistry failed: %v", err), ConditionType: ConditionSyncSuccessful, ConditionReason: conditionReasonFetchFailed, } From 274b8d5a5d16ec4afb0f1db309d0dae3a1235204 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Mon, 17 Nov 2025 19:03:51 +0100 Subject: [PATCH 4/4] renamed test file Signed-off-by: Daniele Martinoli --- internal/registry/{server_registry_test.go => temporary_test.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename internal/registry/{server_registry_test.go => temporary_test.go} (100%) diff --git a/internal/registry/server_registry_test.go b/internal/registry/temporary_test.go similarity index 100% rename from internal/registry/server_registry_test.go rename to internal/registry/temporary_test.go