diff --git a/cmd/devnet-builder/main.go b/cmd/devnet-builder/main.go index 47c12f3d..628b8527 100644 --- a/cmd/devnet-builder/main.go +++ b/cmd/devnet-builder/main.go @@ -22,6 +22,7 @@ var globalLoader *plugin.Loader func main() { // Enable color output color.NoColor = false + logger := output.NewLogger() // Load plugins from ~/.devnet-builder/plugins/ globalLoader = plugin.NewLoader() @@ -29,7 +30,7 @@ func main() { // Load all discovered plugins with detailed error information loadResult, loadErr := globalLoader.LoadAllWithErrors() if loadErr != nil { - output.DefaultLogger.Debug("Plugin loading error: %v", loadErr) + logger.Debug("Plugin loading error: %v", loadErr) } // Log which plugins were successfully loaded @@ -38,11 +39,11 @@ func main() { for _, p := range loadResult.Loaded { loadedNames = append(loadedNames, p.Name()) } - output.DefaultLogger.Debug("Successfully loaded %d plugins: %v", len(loadResult.Loaded), loadedNames) + logger.Debug("Successfully loaded %d plugins: %v", len(loadResult.Loaded), loadedNames) // Log detailed errors for plugins that failed to load for _, loadErr := range loadResult.Errors { - output.DefaultLogger.Warn("Failed to load plugin %q: %v", loadErr.PluginName, loadErr.Err) + logger.Warn("Failed to load plugin %q: %v", loadErr.PluginName, loadErr.Err) } } @@ -57,7 +58,7 @@ func main() { // Create an adapter to convert pkg/network.Module to internal/network.NetworkModule adapter := newPluginAdapter(p.Module()) if err := network.MustRegister(adapter, false); err != nil { - output.DefaultLogger.Warn("Failed to register plugin %q: %v", p.Name(), err) + logger.Warn("Failed to register plugin %q: %v", p.Name(), err) } } @@ -67,7 +68,7 @@ func main() { if envHome := os.Getenv("DEVNET_HOME"); envHome != "" { homeDir = envHome } - if err := checkAndMigrateVersion(homeDir); err != nil { + if err := checkAndMigrateVersion(homeDir, logger); err != nil { fmt.Fprintf(os.Stderr, "Version migration failed: %v\n", err) globalLoader.Close() os.Exit(1) @@ -103,10 +104,7 @@ func GetPluginLoader() *plugin.Loader { } // checkAndMigrateVersion checks the current version and applies migrations if needed. -func checkAndMigrateVersion(homeDir string) error { - // Use default logger for migration - logger := output.DefaultLogger - +func checkAndMigrateVersion(homeDir string, logger *output.Logger) error { // Create infrastructure factory factory := di.NewInfrastructureFactory(homeDir, logger) diff --git a/internal/application/service.go b/internal/application/service.go index 25d6b2a5..3fc59e1b 100644 --- a/internal/application/service.go +++ b/internal/application/service.go @@ -53,7 +53,7 @@ func NewDevnetService(homeDir string, logger *output.Logger, opts ...di.Option) func NewDevnetServiceWithConfig(cfg ServiceConfig) (*DevnetService, error) { logger := cfg.Logger if logger == nil { - logger = output.DefaultLogger + logger = output.NewLogger() } // Create infrastructure factory @@ -818,38 +818,17 @@ func (e *NodeNotFoundError) Error() string { } // GetService returns a DevnetService instance using global homeDir. -// It automatically loads the network module if devnet exists with stored blockchain network. func GetService(homeDir string) (*DevnetService, error) { - // Try to load network module from existing devnet metadata - var networkModule network.NetworkModule - - // Check if devnet exists and load its metadata to get blockchain network - metadataPath := paths.DevnetMetadataPath(homeDir) - if data, err := os.ReadFile(metadataPath); err == nil { - var meta struct { - BlockchainNetwork string `json:"blockchain_network"` - } - if json.Unmarshal(data, &meta) == nil && meta.BlockchainNetwork != "" { - if module, err := network.Get(meta.BlockchainNetwork); err == nil { - networkModule = module - } - } - } - - if networkModule != nil { - return GetServiceWithConfig(ServiceConfig{ - HomeDir: homeDir, - NetworkModule: networkModule, - }) - } - return NewDevnetService(homeDir, output.DefaultLogger) + return GetServiceWithConfig(ServiceConfig{ + HomeDir: homeDir, + }) } // GetServiceWithConfig returns a DevnetService with full configuration. // Use this when you need to specify network module, docker mode, etc. func GetServiceWithConfig(cfg ServiceConfig) (*DevnetService, error) { if cfg.Logger == nil { - cfg.Logger = output.DefaultLogger + cfg.Logger = output.NewLogger() } return NewDevnetServiceWithConfig(cfg) } diff --git a/internal/application/version/service.go b/internal/application/version/service.go index b0904784..96731443 100644 --- a/internal/application/version/service.go +++ b/internal/application/version/service.go @@ -28,7 +28,7 @@ type Service struct { // NewService creates a new migration service. func NewService(repository ports.VersionRepository, logger *output.Logger) *Service { if logger == nil { - logger = output.DefaultLogger + logger = output.NewLogger() } return &Service{ repository: repository, diff --git a/internal/daemon/server/network_service.go b/internal/daemon/server/network_service.go index bc772857..ab28173f 100644 --- a/internal/daemon/server/network_service.go +++ b/internal/daemon/server/network_service.go @@ -16,13 +16,18 @@ type NetworkService struct { v1.UnimplementedNetworkServiceServer githubFactory GitHubClientFactory logger *slog.Logger + registry network.Registry } // NewNetworkService creates a new NetworkService. -func NewNetworkService(githubFactory GitHubClientFactory) *NetworkService { +func NewNetworkService(githubFactory GitHubClientFactory, registry network.Registry) *NetworkService { + if registry == nil { + registry = network.GlobalRegistry() + } return &NetworkService{ githubFactory: githubFactory, logger: slog.Default(), + registry: registry, } } @@ -33,7 +38,7 @@ func (s *NetworkService) SetLogger(logger *slog.Logger) { // ListNetworks returns all registered network modules. func (s *NetworkService) ListNetworks(ctx context.Context, req *v1.ListNetworksRequest) (*v1.ListNetworksResponse, error) { - modules := network.ListModules() + modules := s.registry.ListModules() summaries := make([]*v1.NetworkSummary, 0, len(modules)) for _, module := range modules { @@ -51,7 +56,7 @@ func (s *NetworkService) GetNetworkInfo(ctx context.Context, req *v1.GetNetworkI return nil, status.Error(codes.InvalidArgument, "name is required") } - module, err := network.Get(req.Name) + module, err := s.registry.Get(req.Name) if err != nil { return nil, status.Errorf(codes.NotFound, "network %q not found: %v", req.Name, err) } @@ -129,7 +134,7 @@ func (s *NetworkService) ListBinaryVersions(ctx context.Context, req *v1.ListBin } // Get network module - module, err := network.Get(req.NetworkName) + module, err := s.registry.Get(req.NetworkName) if err != nil { return nil, status.Errorf(codes.NotFound, "network %q not found: %v", req.NetworkName, err) } diff --git a/internal/daemon/server/network_service_test.go b/internal/daemon/server/network_service_test.go index 0ea54458..d6abca45 100644 --- a/internal/daemon/server/network_service_test.go +++ b/internal/daemon/server/network_service_test.go @@ -52,7 +52,7 @@ func (f *mockGitHubClientFactory) CreateClient(networkName, owner, repo string) func TestNetworkService_ListBinaryVersions_MissingNetworkName(t *testing.T) { factory := &mockGitHubClientFactory{} - svc := NewNetworkService(factory) + svc := NewNetworkService(factory, network.GlobalRegistry()) _, err := svc.ListBinaryVersions(context.Background(), &v1.ListBinaryVersionsRequest{ NetworkName: "", @@ -65,7 +65,7 @@ func TestNetworkService_ListBinaryVersions_MissingNetworkName(t *testing.T) { func TestNetworkService_ListBinaryVersions_NetworkNotFound(t *testing.T) { factory := &mockGitHubClientFactory{} - svc := NewNetworkService(factory) + svc := NewNetworkService(factory, network.GlobalRegistry()) _, err := svc.ListBinaryVersions(context.Background(), &v1.ListBinaryVersionsRequest{ NetworkName: "nonexistent-network", @@ -78,7 +78,7 @@ func TestNetworkService_ListBinaryVersions_NetworkNotFound(t *testing.T) { func TestNetworkService_ListBinaryVersions_NilFactory(t *testing.T) { // Test that nil factory is handled gracefully - svc := NewNetworkService(nil) + svc := NewNetworkService(nil, network.GlobalRegistry()) // This will fail at network lookup stage for non-existent network // but if we had a registered network with GitHub source, it should catch nil factory @@ -130,7 +130,7 @@ func TestNetworkService_ListBinaryVersions_Success(t *testing.T) { } factory := &mockGitHubClientFactory{client: mockClient} - svc := NewNetworkService(factory) + svc := NewNetworkService(factory, network.GlobalRegistry()) // Test without prereleases resp, err := svc.ListBinaryVersions(context.Background(), &v1.ListBinaryVersionsRequest{ @@ -182,7 +182,7 @@ func TestNetworkService_ListBinaryVersions_IncludePrerelease(t *testing.T) { } factory := &mockGitHubClientFactory{client: mockClient} - svc := NewNetworkService(factory) + svc := NewNetworkService(factory, network.GlobalRegistry()) // Test with prereleases resp, err := svc.ListBinaryVersions(context.Background(), &v1.ListBinaryVersionsRequest{ @@ -209,7 +209,7 @@ func TestNetworkService_ListBinaryVersions_FetchError(t *testing.T) { } factory := &mockGitHubClientFactory{client: mockClient} - svc := NewNetworkService(factory) + svc := NewNetworkService(factory, network.GlobalRegistry()) _, err := svc.ListBinaryVersions(context.Background(), &v1.ListBinaryVersionsRequest{ NetworkName: testModule.Name(), @@ -231,7 +231,7 @@ func TestNetworkService_ListBinaryVersions_EmptyReleases(t *testing.T) { } factory := &mockGitHubClientFactory{client: mockClient} - svc := NewNetworkService(factory) + svc := NewNetworkService(factory, network.GlobalRegistry()) resp, err := svc.ListBinaryVersions(context.Background(), &v1.ListBinaryVersionsRequest{ NetworkName: testModule.Name(), @@ -264,7 +264,7 @@ func TestNetworkService_ListBinaryVersions_OnlyPrereleases(t *testing.T) { } factory := &mockGitHubClientFactory{client: mockClient} - svc := NewNetworkService(factory) + svc := NewNetworkService(factory, network.GlobalRegistry()) // Without prereleases - should return empty resp, err := svc.ListBinaryVersions(context.Background(), &v1.ListBinaryVersionsRequest{ diff --git a/internal/daemon/server/plugins.go b/internal/daemon/server/plugins.go index e4269509..65fb5e9d 100644 --- a/internal/daemon/server/plugins.go +++ b/internal/daemon/server/plugins.go @@ -13,8 +13,9 @@ import ( // PluginManager handles plugin discovery, loading, and registration. type PluginManager struct { - loader *plugin.Loader - logger *slog.Logger + loader *plugin.Loader + logger *slog.Logger + registry network.Registry } // PluginManagerConfig configures the PluginManager. @@ -25,6 +26,10 @@ type PluginManagerConfig struct { // Logger for logging plugin operations. Logger *slog.Logger + + // NetworkRegistry receives discovered plugin modules. + // If nil, the package-global registry is used for backward compatibility. + NetworkRegistry network.Registry } // NewPluginManager creates a new PluginManager. @@ -33,6 +38,10 @@ func NewPluginManager(config PluginManagerConfig) *PluginManager { if logger == nil { logger = slog.Default() } + registry := config.NetworkRegistry + if registry == nil { + registry = network.GlobalRegistry() + } // Create hashicorp logger adapter hcLogger := hclog.New(&hclog.LoggerOptions{ @@ -49,8 +58,9 @@ func NewPluginManager(config PluginManagerConfig) *PluginManager { } return &PluginManager{ - loader: plugin.NewLoader(opts...), - logger: logger, + loader: plugin.NewLoader(opts...), + logger: logger, + registry: registry, } } @@ -69,7 +79,7 @@ type PluginLoadError struct { Error error } -// LoadAndRegister discovers plugins, loads them, and registers them with the global network registry. +// LoadAndRegister discovers plugins, loads them, and registers them with the injected network registry. // Returns the load results including any errors encountered. func (pm *PluginManager) LoadAndRegister() (*LoadResult, error) { result := &LoadResult{ @@ -91,7 +101,7 @@ func (pm *PluginManager) LoadAndRegister() (*LoadResult, error) { pm.logger.Info("discovered plugins", "count", len(discovered), "plugins", discovered) - // Load each plugin and register with global registry + // Load each plugin and register with injected registry for _, name := range discovered { if err := pm.loadAndRegisterPlugin(name); err != nil { pm.logger.Warn("failed to load plugin", @@ -111,7 +121,7 @@ func (pm *PluginManager) LoadAndRegister() (*LoadResult, error) { return result, nil } -// loadAndRegisterPlugin loads a single plugin and registers it with the global registry. +// loadAndRegisterPlugin loads a single plugin and registers it with the injected registry. func (pm *PluginManager) loadAndRegisterPlugin(name string) error { // Load the plugin client, err := pm.loader.Load(name) @@ -125,8 +135,8 @@ func (pm *PluginManager) loadAndRegisterPlugin(name string) error { // Wrap with adapter to convert pkg/network.Module to internal/network.NetworkModule adapter := network.NewPluginAdapter(module) - // Register with global registry - if err := network.MustRegister(adapter, false); err != nil { + // Register with injected registry + if err := pm.registry.MustRegister(adapter, false); err != nil { return fmt.Errorf("failed to register plugin module: %w", err) } @@ -157,11 +167,11 @@ func (pm *PluginManager) Reload(name string) error { // Wrap with adapter adapter := network.NewPluginAdapter(module) - // Re-register with global registry + // Re-register with injected registry // Note: The registry doesn't support re-registration, so this may fail // if the module was previously registered. In production, you'd want // a mechanism to update existing registrations. - if err := network.MustRegister(adapter, false); err != nil { + if err := pm.registry.MustRegister(adapter, false); err != nil { pm.logger.Warn("plugin reloaded but registry update failed (already registered)", "plugin", name, "error", err) diff --git a/internal/daemon/server/server.go b/internal/daemon/server/server.go index c6f89aac..646bb87a 100644 --- a/internal/daemon/server/server.go +++ b/internal/daemon/server/server.go @@ -26,6 +26,7 @@ import ( "github.com/altuslabsxyz/devnet-builder/internal/daemon/subnet" "github.com/altuslabsxyz/devnet-builder/internal/daemon/types" "github.com/altuslabsxyz/devnet-builder/internal/daemon/upgrader" + "github.com/altuslabsxyz/devnet-builder/internal/infrastructure/network" "google.golang.org/grpc" ) @@ -93,6 +94,7 @@ type Server struct { manager *controller.Manager healthCtrl *controller.HealthController pluginManager *PluginManager + networkRegistry network.Registry subnetAllocator *subnet.Allocator nodeRuntime runtime.NodeRuntime // Node runtime for process management grpcServer *grpc.Server @@ -136,12 +138,15 @@ func New(config *Config) (*Server, error) { multiWriter := io.MultiWriter(os.Stdout, logFile) logger := slog.New(slog.NewTextHandler(multiWriter, &slog.HandlerOptions{Level: level})) + networkRegistry := network.NewRegistry() + // Load network plugins from plugin directories // Plugins are discovered from ~/.devnet-builder/plugins/ and registered - // with the global network registry so they can be queried via NetworkService + // with the injected network registry so they can be queried via NetworkService pluginMgr := NewPluginManager(PluginManagerConfig{ - PluginDirs: []string{filepath.Join(config.DataDir, "plugins")}, - Logger: logger, + PluginDirs: []string{filepath.Join(config.DataDir, "plugins")}, + Logger: logger, + NetworkRegistry: networkRegistry, }) result, err := pluginMgr.LoadAndRegister() @@ -185,7 +190,7 @@ func New(config *Config) (*Server, error) { mgr.SetLogger(logger) // Create orchestrator factory for full provisioning flow (build, fork, init) - orchFactory := NewOrchestratorFactory(config.DataDir, logger) + orchFactory := NewOrchestratorFactory(config.DataDir, logger, networkRegistry) // Create devnet provisioner with orchestrator factory and subnet allocator // The factory enables full provisioning (build, fork, init) before creating Node resources @@ -324,7 +329,7 @@ func New(config *Config) (*Server, error) { // Create network service first (needed by ante handler) githubFactory := NewDefaultGitHubClientFactory(config.DataDir, logger) - networkSvc := NewNetworkService(githubFactory) + networkSvc := NewNetworkService(githubFactory, networkRegistry) networkSvc.SetLogger(logger) // Create ante handler for request validation @@ -364,6 +369,7 @@ func New(config *Config) (*Server, error) { manager: mgr, healthCtrl: healthCtrl, pluginManager: pluginMgr, + networkRegistry: networkRegistry, subnetAllocator: subnetAlloc, nodeRuntime: nodeRuntime, grpcServer: grpcServer, diff --git a/internal/daemon/server/wiring.go b/internal/daemon/server/wiring.go index f3e7d718..a92c331c 100644 --- a/internal/daemon/server/wiring.go +++ b/internal/daemon/server/wiring.go @@ -1,7 +1,7 @@ // internal/daemon/server/wiring.go // Package server provides wiring for the daemon server. // This file contains dependency injection for the provisioning system, -// using NetworkModule from the global registry (populated by loaded plugins). +// using NetworkModule from an injected registry (populated by loaded plugins). package server import ( @@ -721,23 +721,28 @@ func (a *nodeInitializerAdapter) GetTestMnemonic(validatorIndex int) string { // ============================================================================= // OrchestratorFactory creates orchestrators for the daemon. -// It uses the global network registry to obtain NetworkModules from loaded plugins. +// It uses an injected network registry to obtain NetworkModules from loaded plugins. type OrchestratorFactory struct { - dataDir string - logger *slog.Logger + dataDir string + logger *slog.Logger + registry network.Registry } // NewOrchestratorFactory creates a new orchestrator factory. -func NewOrchestratorFactory(dataDir string, logger *slog.Logger) *OrchestratorFactory { +func NewOrchestratorFactory(dataDir string, logger *slog.Logger, registry network.Registry) *OrchestratorFactory { + if registry == nil { + registry = network.GlobalRegistry() + } return &OrchestratorFactory{ - dataDir: dataDir, - logger: logger, + dataDir: dataDir, + logger: logger, + registry: registry, } } // GetBuilder implements builder.PluginLoader interface. func (f *OrchestratorFactory) GetBuilder(pluginName string) (plugintypes.PluginBuilder, error) { - module, err := network.Get(pluginName) + module, err := f.registry.Get(pluginName) if err != nil { return nil, err } @@ -746,7 +751,7 @@ func (f *OrchestratorFactory) GetBuilder(pluginName string) (plugintypes.PluginB // GetPluginRuntime returns the PluginRuntime for a network. func (f *OrchestratorFactory) GetPluginRuntime(pluginName string) (runtime.PluginRuntime, error) { - module, err := network.Get(pluginName) + module, err := f.registry.Get(pluginName) if err != nil { return nil, err } @@ -781,7 +786,7 @@ func (f *OrchestratorFactory) AsPluginRuntimeProvider() runtime.PluginRuntimePro // (SkipStart=true in ProvisionOptions), so NodeRuntime is not needed. // Returns provisioner.Orchestrator interface for testability. func (f *OrchestratorFactory) CreateOrchestrator(networkName string) (provisioner.Orchestrator, error) { - module, err := network.Get(networkName) + module, err := f.registry.Get(networkName) if err != nil { return nil, err } @@ -830,13 +835,13 @@ func (f *OrchestratorFactory) CreateOrchestrator(networkName string) (provisione // ListAvailableNetworks returns the names of all registered networks. func (f *OrchestratorFactory) ListAvailableNetworks() []string { - return network.List() + return f.registry.List() } // GetNetworkDefaults returns default URLs for a network/plugin. // Implements provisioner.OrchestratorFactory interface. func (f *OrchestratorFactory) GetNetworkDefaults(pluginName, networkType string) (*provisioner.NetworkDefaults, error) { - module, err := network.Get(pluginName) + module, err := f.registry.Get(pluginName) if err != nil { return nil, err } diff --git a/internal/daemon/server/wiring_test.go b/internal/daemon/server/wiring_test.go index c01daca2..44da7ee0 100644 --- a/internal/daemon/server/wiring_test.go +++ b/internal/daemon/server/wiring_test.go @@ -144,7 +144,7 @@ var _ network.NetworkModule = (*mockNetworkModule)(nil) func TestNewOrchestratorFactory(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - f := NewOrchestratorFactory("/tmp/test-data", logger) + f := NewOrchestratorFactory("/tmp/test-data", logger, network.NewRegistry()) require.NotNil(t, f) assert.Equal(t, "/tmp/test-data", f.dataDir) @@ -153,7 +153,7 @@ func TestNewOrchestratorFactory(t *testing.T) { func TestOrchestratorFactory_GetBuilder_UnknownNetwork(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - f := NewOrchestratorFactory("/tmp/test-data", logger) + f := NewOrchestratorFactory("/tmp/test-data", logger, network.NewRegistry()) // Should fail for unknown network (no plugins loaded) builder, err := f.GetBuilder("nonexistent-network") @@ -163,7 +163,7 @@ func TestOrchestratorFactory_GetBuilder_UnknownNetwork(t *testing.T) { func TestOrchestratorFactory_GetPluginRuntime_UnknownNetwork(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - f := NewOrchestratorFactory("/tmp/test-data", logger) + f := NewOrchestratorFactory("/tmp/test-data", logger, network.NewRegistry()) // Should fail for unknown network pr, err := f.GetPluginRuntime("nonexistent-network") @@ -173,7 +173,7 @@ func TestOrchestratorFactory_GetPluginRuntime_UnknownNetwork(t *testing.T) { func TestOrchestratorFactory_CreateOrchestrator_UnknownNetwork(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - f := NewOrchestratorFactory("/tmp/test-data", logger) + f := NewOrchestratorFactory("/tmp/test-data", logger, network.NewRegistry()) // Should fail for unknown network orch, err := f.CreateOrchestrator("nonexistent-network") @@ -183,7 +183,7 @@ func TestOrchestratorFactory_CreateOrchestrator_UnknownNetwork(t *testing.T) { func TestOrchestratorFactory_ListAvailableNetworks_Empty(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - f := NewOrchestratorFactory("/tmp/test-data", logger) + f := NewOrchestratorFactory("/tmp/test-data", logger, network.NewRegistry()) // Should return empty list when no plugins are loaded networks := f.ListAvailableNetworks() diff --git a/internal/di/container.go b/internal/di/container.go index 7647352e..d3989977 100644 --- a/internal/di/container.go +++ b/internal/di/container.go @@ -114,36 +114,46 @@ type Config struct { // NetworkRegistry wraps network registration operations. // This provides an injectable alternative to the global registry. -type NetworkRegistry struct{} +type NetworkRegistry struct { + registry network.Registry +} + +// NewNetworkRegistry creates a new wrapper for an injected network registry. +func NewNetworkRegistry(registry network.Registry) *NetworkRegistry { + if registry == nil { + registry = network.GlobalRegistry() + } + return &NetworkRegistry{registry: registry} +} // Get retrieves a network module by name. func (r *NetworkRegistry) Get(name string) (network.NetworkModule, error) { - return network.Get(name) + return r.registry.Get(name) } // Has checks if a network is registered. func (r *NetworkRegistry) Has(name string) bool { - return network.Has(name) + return r.registry.Has(name) } // List returns all registered network names. func (r *NetworkRegistry) List() []string { - return network.List() + return r.registry.List() } // ListModules returns all registered network modules. func (r *NetworkRegistry) ListModules() []network.NetworkModule { - return network.ListModules() + return r.registry.ListModules() } // Default returns the default network module. func (r *NetworkRegistry) Default() (network.NetworkModule, error) { - return network.Default() + return r.registry.Default() } // SetDefault changes the default network name. func (r *NetworkRegistry) SetDefault(name string) error { - return network.SetDefault(name) + return r.registry.SetDefault(name) } // Option is a function that configures the container. @@ -317,11 +327,18 @@ func WithBinaryVersionDetector(detector ports.BinaryVersionDetector) Option { } } +// WithNetworkRegistry sets an instance-scoped network registry. +func WithNetworkRegistry(registry network.Registry) Option { + return func(c *Container) { + c.networkReg = NewNetworkRegistry(registry) + } +} + // New creates a new dependency injection container with the given options. func New(opts ...Option) *Container { c := &Container{ logger: output.NewLogger(), - networkReg: &NetworkRegistry{}, + networkReg: NewNetworkRegistry(network.GlobalRegistry()), config: &Config{}, } diff --git a/internal/di/container_v2.go b/internal/di/container_v2.go index 43b7ea74..dc1ac450 100644 --- a/internal/di/container_v2.go +++ b/internal/di/container_v2.go @@ -10,7 +10,6 @@ import ( "github.com/altuslabsxyz/devnet-builder/internal/application/ports" "github.com/altuslabsxyz/devnet-builder/internal/application/upgrade" "github.com/altuslabsxyz/devnet-builder/internal/di/providers" - "github.com/altuslabsxyz/devnet-builder/internal/infrastructure/network" "github.com/altuslabsxyz/devnet-builder/internal/infrastructure/plugin" "github.com/altuslabsxyz/devnet-builder/internal/output" ) @@ -76,11 +75,18 @@ func WithInfrastructureV2(infra providers.InfrastructureProvider) OptionV2 { } } +// WithNetworkRegistryV2 sets an instance-scoped network registry wrapper. +func WithNetworkRegistryV2(registry *NetworkRegistry) OptionV2 { + return func(c *ContainerV2) { + c.networkReg = registry + } +} + // NewV2 creates a new ContainerV2 with the given options. func NewV2(opts ...OptionV2) *ContainerV2 { c := &ContainerV2{ logger: output.NewLogger(), - networkReg: &NetworkRegistry{}, + networkReg: NewNetworkRegistry(nil), config: &Config{}, } @@ -432,44 +438,6 @@ func (c *ContainerV2) SetBinaryResolver(resolver ports.BinaryResolver) { "Set the binary resolver when creating the InfrastructureProvider instead.") } -// ───────────────────────────────────────────────────────────────────────────── -// NetworkRegistry Wrapper (unchanged from original) -// ───────────────────────────────────────────────────────────────────────────── - -// NetworkRegistryV2 wraps network registration operations. -// This provides an injectable alternative to the global registry. -type NetworkRegistryV2 struct{} - -// Get retrieves a network module by name. -func (r *NetworkRegistryV2) Get(name string) (network.NetworkModule, error) { - return network.Get(name) -} - -// Has checks if a network is registered. -func (r *NetworkRegistryV2) Has(name string) bool { - return network.Has(name) -} - -// List returns all registered network names. -func (r *NetworkRegistryV2) List() []string { - return network.List() -} - -// ListModules returns all registered network modules. -func (r *NetworkRegistryV2) ListModules() []network.NetworkModule { - return network.ListModules() -} - -// Default returns the default network module. -func (r *NetworkRegistryV2) Default() (network.NetworkModule, error) { - return network.Default() -} - -// SetDefault changes the default network name. -func (r *NetworkRegistryV2) SetDefault(name string) error { - return network.SetDefault(name) -} - // ───────────────────────────────────────────────────────────────────────────── // Compatibility Layer // ───────────────────────────────────────────────────────────────────────────── diff --git a/internal/infrastructure/builder/builder.go b/internal/infrastructure/builder/builder.go index efd90c49..e93e016d 100644 --- a/internal/infrastructure/builder/builder.go +++ b/internal/infrastructure/builder/builder.go @@ -120,7 +120,7 @@ type Builder struct { // A NetworkModule is required for building - use network plugins to provide one. func NewBuilder(homeDir string, logger *output.Logger, networkModule network.NetworkModule) *Builder { if logger == nil { - logger = output.DefaultLogger + logger = output.NewLogger() } return &Builder{ homeDir: homeDir, diff --git a/internal/infrastructure/builder/builder_adapter.go b/internal/infrastructure/builder/builder_adapter.go index 626ab1e4..f6382f19 100644 --- a/internal/infrastructure/builder/builder_adapter.go +++ b/internal/infrastructure/builder/builder_adapter.go @@ -22,7 +22,7 @@ type BuilderAdapter struct { // NewBuilderAdapter creates a new BuilderAdapter. func NewBuilderAdapter(homeDir string, logger *output.Logger, module network.NetworkModule) *BuilderAdapter { if logger == nil { - logger = output.DefaultLogger + logger = output.NewLogger() } var b *Builder diff --git a/internal/infrastructure/cache/binary_cache.go b/internal/infrastructure/cache/binary_cache.go index b862a565..f42ee42f 100644 --- a/internal/infrastructure/cache/binary_cache.go +++ b/internal/infrastructure/cache/binary_cache.go @@ -21,7 +21,7 @@ type BinaryCache struct { func NewBinaryCache(homeDir, binaryName string, logger *output.Logger) *BinaryCache { if logger == nil { - logger = output.DefaultLogger + logger = output.NewLogger() } if binaryName == "" { binaryName = paths.DefaultBinaryName diff --git a/internal/infrastructure/docker/orchestrator.go b/internal/infrastructure/docker/orchestrator.go index daaf0922..68ab4d00 100644 --- a/internal/infrastructure/docker/orchestrator.go +++ b/internal/infrastructure/docker/orchestrator.go @@ -32,7 +32,7 @@ func NewOrchestrator( logger *output.Logger, ) *OrchestratorImpl { if logger == nil { - logger = output.DefaultLogger + logger = output.NewLogger() } // Default state directory: ~/.devnet-builder/state @@ -57,7 +57,7 @@ func NewOrchestratorWithStateDir( logger *output.Logger, ) *OrchestratorImpl { if logger == nil { - logger = output.DefaultLogger + logger = output.NewLogger() } return &OrchestratorImpl{ networkManager: networkManager, diff --git a/internal/infrastructure/genesis/fetcher.go b/internal/infrastructure/genesis/fetcher.go index 26641afe..1242239b 100644 --- a/internal/infrastructure/genesis/fetcher.go +++ b/internal/infrastructure/genesis/fetcher.go @@ -29,7 +29,7 @@ type FetcherAdapter struct { // NewFetcherAdapter creates a new FetcherAdapter. func NewFetcherAdapter(homeDir, binaryPath, dockerImage string, useDocker bool, logger *output.Logger) *FetcherAdapter { if logger == nil { - logger = output.DefaultLogger + logger = output.NewLogger() } return &FetcherAdapter{ homeDir: homeDir, diff --git a/internal/infrastructure/network/registry.go b/internal/infrastructure/network/registry.go index 08f0b708..d4421c50 100644 --- a/internal/infrastructure/network/registry.go +++ b/internal/infrastructure/network/registry.go @@ -8,6 +8,19 @@ import ( // DefaultNetworkName is empty - networks are loaded dynamically via plugins. const DefaultNetworkName = "" +// Registry defines instance-scoped network registry operations. +// Callers should depend on this interface instead of package globals. +type Registry interface { + Register(module NetworkModule) error + MustRegister(module NetworkModule, panicOnError bool) error + Get(name string) (NetworkModule, error) + Has(name string) bool + List() []string + ListModules() []NetworkModule + Default() (NetworkModule, error) + SetDefault(name string) error +} + // Global registry instance // // Deprecated: Global registry is provided for backward compatibility. @@ -20,10 +33,16 @@ var ( // NewRegistry creates a new registry instance. // Use this when you need an independent registry (e.g., for testing). -func NewRegistry() *registry { +func NewRegistry() Registry { return newRegistry() } +// GlobalRegistry returns the package-level registry instance. +// This exists for backward compatibility and explicit wiring into DI. +func GlobalRegistry() Registry { + return globalRegistry +} + // registry holds registered network modules. type registry struct { mu sync.RWMutex @@ -45,7 +64,7 @@ func newRegistry() *registry { // - A module with the same name is already registered // - The module fails validation func Register(module NetworkModule) { - if err := globalRegistry.register(module); err != nil { + if err := globalRegistry.Register(module); err != nil { panic(err) } } @@ -53,17 +72,13 @@ func Register(module NetworkModule) { // MustRegister is like Register but allows specifying whether to panic on error. // If panicOnError is false, errors are silently ignored. func MustRegister(module NetworkModule, panicOnError bool) error { - err := globalRegistry.register(module) - if err != nil && panicOnError { - panic(err) - } - return err + return globalRegistry.MustRegister(module, panicOnError) } // Get retrieves a network module by name from the global registry. // Returns an error if the network is not registered. func Get(name string) (NetworkModule, error) { - return globalRegistry.get(name) + return globalRegistry.Get(name) } // MustGet retrieves a network module by name, panicking if not found. @@ -77,33 +92,77 @@ func MustGet(name string) NetworkModule { // Has checks if a network is registered. func Has(name string) bool { - return globalRegistry.has(name) + return globalRegistry.Has(name) } // List returns all registered network names in sorted order. func List() []string { - return globalRegistry.list() + return globalRegistry.List() } // ListModules returns all registered network modules. func ListModules() []NetworkModule { - return globalRegistry.listModules() + return globalRegistry.ListModules() } // Default returns the default network module ("stable"). // Returns an error if the default network is not registered. func Default() (NetworkModule, error) { - return globalRegistry.defaults_() + return globalRegistry.Default() } // SetDefault changes the default network name. // Returns an error if the network is not registered. func SetDefault(name string) error { - return globalRegistry.setDefault(name) + return globalRegistry.SetDefault(name) } // Registry methods +// Register adds a module to this registry instance. +func (r *registry) Register(module NetworkModule) error { + return r.register(module) +} + +// MustRegister adds a module and optionally panics on error. +func (r *registry) MustRegister(module NetworkModule, panicOnError bool) error { + err := r.register(module) + if err != nil && panicOnError { + panic(err) + } + return err +} + +// Get returns a module by name from this registry instance. +func (r *registry) Get(name string) (NetworkModule, error) { + return r.get(name) +} + +// Has reports whether a module exists in this registry instance. +func (r *registry) Has(name string) bool { + return r.has(name) +} + +// List returns all module names in this registry instance. +func (r *registry) List() []string { + return r.list() +} + +// ListModules returns all modules in this registry instance. +func (r *registry) ListModules() []NetworkModule { + return r.listModules() +} + +// Default returns the default module in this registry instance. +func (r *registry) Default() (NetworkModule, error) { + return r.defaults_() +} + +// SetDefault changes the default module name in this registry instance. +func (r *registry) SetDefault(name string) error { + return r.setDefault(name) +} + func (r *registry) register(module NetworkModule) error { if module == nil { return &ModuleValidationError{ @@ -212,10 +271,14 @@ func (r *registry) setDefault(name string) error { return nil } +func (r *registry) reset() { + r.mu.Lock() + defer r.mu.Unlock() + r.modules = make(map[string]NetworkModule) + r.defaults = DefaultNetworkName +} + // ResetRegistry clears all registered modules. This is primarily for testing. func ResetRegistry() { - globalRegistry.mu.Lock() - defer globalRegistry.mu.Unlock() - globalRegistry.modules = make(map[string]NetworkModule) - globalRegistry.defaults = DefaultNetworkName + globalRegistry.reset() } diff --git a/internal/infrastructure/node/docker.go b/internal/infrastructure/node/docker.go index d51eab03..70090f11 100644 --- a/internal/infrastructure/node/docker.go +++ b/internal/infrastructure/node/docker.go @@ -79,7 +79,7 @@ func NewDockerManager(image string, logger *output.Logger) *DockerManager { image = DefaultDockerImage } if logger == nil { - logger = output.DefaultLogger + logger = output.NewLogger() } return &DockerManager{ Image: image, diff --git a/internal/infrastructure/node/factory.go b/internal/infrastructure/node/factory.go index ebfacc9e..6658743a 100644 --- a/internal/infrastructure/node/factory.go +++ b/internal/infrastructure/node/factory.go @@ -23,7 +23,7 @@ type FactoryConfig struct { // EVMChainID is the EVM chain ID (optional, used for --evm.evm-chain-id flag). EVMChainID string - // Logger is the output logger. If nil, uses DefaultLogger. + // Logger is the output logger. If nil, a new logger is created. Logger *output.Logger } @@ -59,7 +59,7 @@ func (f *NodeManagerFactory) Create() (NodeManager, error) { logger := f.config.Logger if logger == nil { - logger = output.DefaultLogger + logger = output.NewLogger() } switch f.config.Mode { diff --git a/internal/infrastructure/node/local.go b/internal/infrastructure/node/local.go index 2c48c5e1..45990751 100644 --- a/internal/infrastructure/node/local.go +++ b/internal/infrastructure/node/local.go @@ -36,7 +36,7 @@ func NewLocalManager(binary string, logger *output.Logger) *LocalManager { binary = DefaultLocalBinary } if logger == nil { - logger = output.DefaultLogger + logger = output.NewLogger() } return &LocalManager{ Binary: binary, diff --git a/internal/infrastructure/node/manager.go b/internal/infrastructure/node/manager.go index c661e3cf..8188070e 100644 --- a/internal/infrastructure/node/manager.go +++ b/internal/infrastructure/node/manager.go @@ -27,7 +27,7 @@ func NewLocalNodeManager( logger *output.Logger, ) *LocalNodeManager { if logger == nil { - logger = output.DefaultLogger + logger = output.NewLogger() } return &LocalNodeManager{ @@ -131,7 +131,7 @@ func NewDockerNodeManager( logger *output.Logger, ) *DockerNodeManager { if logger == nil { - logger = output.DefaultLogger + logger = output.NewLogger() } return &DockerNodeManager{ diff --git a/internal/infrastructure/nodeconfig/config.go b/internal/infrastructure/nodeconfig/config.go index 73a10e03..ab26cdc8 100644 --- a/internal/infrastructure/nodeconfig/config.go +++ b/internal/infrastructure/nodeconfig/config.go @@ -32,7 +32,7 @@ type ConfigEditor struct { // NewConfigEditor creates a new ConfigEditor. func NewConfigEditor(nodeDir string, logger *output.Logger) *ConfigEditor { if logger == nil { - logger = output.DefaultLogger + logger = output.NewLogger() } return &ConfigEditor{ nodeDir: nodeDir, diff --git a/internal/infrastructure/nodeconfig/initializer.go b/internal/infrastructure/nodeconfig/initializer.go index 90cca5ca..0557d883 100644 --- a/internal/infrastructure/nodeconfig/initializer.go +++ b/internal/infrastructure/nodeconfig/initializer.go @@ -34,7 +34,7 @@ type NodeInitializer struct { // NewNodeInitializer creates a new NodeInitializer. func NewNodeInitializer(mode types.ExecutionMode, dockerImage string, logger *output.Logger) *NodeInitializer { if logger == nil { - logger = output.DefaultLogger + logger = output.NewLogger() } return &NodeInitializer{ mode: mode, @@ -47,7 +47,7 @@ func NewNodeInitializer(mode types.ExecutionMode, dockerImage string, logger *ou // For local mode, this should be the managed binary at ~/.devnet-builder/bin/stabled. func NewNodeInitializerWithBinary(mode types.ExecutionMode, dockerImage, binaryPath string, logger *output.Logger) *NodeInitializer { if logger == nil { - logger = output.DefaultLogger + logger = output.NewLogger() } return &NodeInitializer{ mode: mode, diff --git a/internal/infrastructure/snapshot/download.go b/internal/infrastructure/snapshot/download.go index e7020840..7a0b7b89 100644 --- a/internal/infrastructure/snapshot/download.go +++ b/internal/infrastructure/snapshot/download.go @@ -42,7 +42,7 @@ type DownloadOptions struct { func Download(ctx context.Context, opts DownloadOptions) (*SnapshotCache, error) { logger := opts.Logger if logger == nil { - logger = output.DefaultLogger + logger = output.NewLogger() } // Check cache first (unless NoCache is set) diff --git a/internal/infrastructure/snapshot/fetcher.go b/internal/infrastructure/snapshot/fetcher.go index 88bb0663..4d2e79ff 100644 --- a/internal/infrastructure/snapshot/fetcher.go +++ b/internal/infrastructure/snapshot/fetcher.go @@ -23,7 +23,7 @@ type FetcherAdapter struct { // NewFetcherAdapter creates a new FetcherAdapter. func NewFetcherAdapter(homeDir string, logger *output.Logger) *FetcherAdapter { if logger == nil { - logger = output.DefaultLogger + logger = output.NewLogger() } return &FetcherAdapter{ homeDir: homeDir, diff --git a/internal/infrastructure/stateexport/adapter.go b/internal/infrastructure/stateexport/adapter.go index 5af3c9f1..cd9549b8 100644 --- a/internal/infrastructure/stateexport/adapter.go +++ b/internal/infrastructure/stateexport/adapter.go @@ -28,7 +28,7 @@ type Adapter struct { // NewAdapter creates a new StateExportAdapter. func NewAdapter(homeDir string, logger *output.Logger) *Adapter { if logger == nil { - logger = output.DefaultLogger + logger = output.NewLogger() } return &Adapter{ homeDir: homeDir, diff --git a/tests/unit/resume_usecase_test.go b/tests/unit/resume_usecase_test.go index 9c7bbe24..f706e461 100644 --- a/tests/unit/resume_usecase_test.go +++ b/tests/unit/resume_usecase_test.go @@ -99,7 +99,7 @@ func TestResumeUseCase_CheckState(t *testing.T) { mockManager := &MockStateManager{State: nil} mockDetector := &MockStateDetector{} transitioner := upgrade.NewStateTransitioner() - logger := output.DefaultLogger + logger := output.NewLogger() uc := upgrade.NewResumeUseCase(mockManager, mockDetector, transitioner, nil, logger) @@ -113,7 +113,7 @@ func TestResumeUseCase_CheckState(t *testing.T) { mockManager := &MockStateManager{State: existingState} mockDetector := &MockStateDetector{} transitioner := upgrade.NewStateTransitioner() - logger := output.DefaultLogger + logger := output.NewLogger() uc := upgrade.NewResumeUseCase(mockManager, mockDetector, transitioner, nil, logger) @@ -130,7 +130,7 @@ func TestResumeUseCase_ClearState(t *testing.T) { mockManager := &MockStateManager{State: existingState} mockDetector := &MockStateDetector{} transitioner := upgrade.NewStateTransitioner() - logger := output.DefaultLogger + logger := output.NewLogger() uc := upgrade.NewResumeUseCase(mockManager, mockDetector, transitioner, nil, logger) @@ -145,7 +145,7 @@ func TestResumeUseCase_Resume_ClearStateOption(t *testing.T) { mockManager := &MockStateManager{State: existingState} mockDetector := &MockStateDetector{} transitioner := upgrade.NewStateTransitioner() - logger := output.DefaultLogger + logger := output.NewLogger() uc := upgrade.NewResumeUseCase(mockManager, mockDetector, transitioner, nil, logger) @@ -165,7 +165,7 @@ func TestResumeUseCase_Resume_ShowStatusOption(t *testing.T) { mockManager := &MockStateManager{State: existingState} mockDetector := &MockStateDetector{} transitioner := upgrade.NewStateTransitioner() - logger := output.DefaultLogger + logger := output.NewLogger() uc := upgrade.NewResumeUseCase(mockManager, mockDetector, transitioner, nil, logger) @@ -185,7 +185,7 @@ func TestResumeUseCase_Resume_ForceRestartOption(t *testing.T) { mockManager := &MockStateManager{State: existingState} mockDetector := &MockStateDetector{} transitioner := upgrade.NewStateTransitioner() - logger := output.DefaultLogger + logger := output.NewLogger() uc := upgrade.NewResumeUseCase(mockManager, mockDetector, transitioner, nil, logger) @@ -203,7 +203,7 @@ func TestResumeUseCase_Resume_NoExistingState(t *testing.T) { mockManager := &MockStateManager{State: nil} mockDetector := &MockStateDetector{} transitioner := upgrade.NewStateTransitioner() - logger := output.DefaultLogger + logger := output.NewLogger() uc := upgrade.NewResumeUseCase(mockManager, mockDetector, transitioner, nil, logger) @@ -246,7 +246,7 @@ func TestResumeUseCase_Resume_TerminalState(t *testing.T) { mockManager := &MockStateManager{State: existingState} mockDetector := &MockStateDetector{} transitioner := upgrade.NewStateTransitioner() - logger := output.DefaultLogger + logger := output.NewLogger() uc := upgrade.NewResumeUseCase(mockManager, mockDetector, transitioner, nil, logger) @@ -267,7 +267,7 @@ func TestResumeUseCase_Resume_CorruptedState(t *testing.T) { } mockDetector := &MockStateDetector{} transitioner := upgrade.NewStateTransitioner() - logger := output.DefaultLogger + logger := output.NewLogger() uc := upgrade.NewResumeUseCase(mockManager, mockDetector, transitioner, nil, logger) @@ -290,7 +290,7 @@ func TestResumeUseCase_Resume_DetectsStageProgression(t *testing.T) { DetectedStage: ports.ResumableStageVoting, } transitioner := upgrade.NewStateTransitioner() - logger := output.DefaultLogger + logger := output.NewLogger() // Note: We can't fully test resume execution without a mock ResumableExecuteUpgradeUseCase // This test verifies the detection and state update logic @@ -312,7 +312,7 @@ func TestResumeUseCase_Reconcile(t *testing.T) { mockManager := &MockStateManager{State: nil} mockDetector := &MockStateDetector{} transitioner := upgrade.NewStateTransitioner() - logger := output.DefaultLogger + logger := output.NewLogger() uc := upgrade.NewResumeUseCase(mockManager, mockDetector, transitioner, nil, logger) @@ -331,7 +331,7 @@ func TestResumeUseCase_Reconcile(t *testing.T) { DetectedStage: ports.ResumableStageWaitingForHeight, } transitioner := upgrade.NewStateTransitioner() - logger := output.DefaultLogger + logger := output.NewLogger() uc := upgrade.NewResumeUseCase(mockManager, mockDetector, transitioner, nil, logger)