From 93b6b8770509b48b013ddd55bb04f4c689226f00 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:12:57 -0500 Subject: [PATCH 1/4] refactor(workstation): Remove dependency injector Factors out the dependency injector from the workstation package. Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/project/project.go | 9 +- pkg/workstation/network/colima_network.go | 50 +-- .../network/colima_network_test.go | 87 ++--- .../network/darwin_network_test.go | 8 +- pkg/workstation/network/linux_network_test.go | 8 +- pkg/workstation/network/mock_network.go | 10 +- pkg/workstation/network/mock_network_test.go | 16 +- pkg/workstation/network/network.go | 51 +-- pkg/workstation/network/network_test.go | 154 +++----- .../network/windows_network_test.go | 8 +- pkg/workstation/services/dns_service.go | 28 +- pkg/workstation/services/dns_service_test.go | 123 +------ .../services/git_livereload_service.go | 11 +- .../services/git_livereload_service_test.go | 51 +-- .../services/localstack_service.go | 8 +- .../services/localstack_service_test.go | 14 +- pkg/workstation/services/registry_service.go | 11 +- .../services/registry_service_test.go | 61 +--- pkg/workstation/services/service.go | 34 +- pkg/workstation/services/service_test.go | 104 ++---- pkg/workstation/services/talos_service.go | 6 +- .../services/talos_service_test.go | 119 ++---- pkg/workstation/virt/colima_virt.go | 19 +- pkg/workstation/virt/colima_virt_test.go | 106 +++--- pkg/workstation/virt/docker_virt.go | 188 +++------- pkg/workstation/virt/docker_virt_test.go | 345 +++++++----------- pkg/workstation/virt/virt.go | 36 +- pkg/workstation/virt/virt_test.go | 91 ++--- pkg/workstation/workstation.go | 139 +++---- pkg/workstation/workstation_test.go | 337 +++++++++-------- 30 files changed, 766 insertions(+), 1466 deletions(-) diff --git a/pkg/project/project.go b/pkg/project/project.go index 17d6df5e5..b9eaf1005 100644 --- a/pkg/project/project.go +++ b/pkg/project/project.go @@ -84,10 +84,7 @@ func NewProject(injector di.Injector, contextName string, opts ...*Project) (*Pr if overrides != nil && overrides.Workstation != nil { ws = overrides.Workstation } else { - workstationCtx := &workstation.WorkstationRuntime{ - Runtime: *rt, - } - ws, err = workstation.NewWorkstation(workstationCtx, rt.Injector) + ws, err = workstation.NewWorkstation(rt) if err != nil { return nil, fmt.Errorf("failed to create workstation: %w", err) } @@ -157,8 +154,8 @@ func (p *Project) Configure(flagOverrides map[string]any) error { // if any step fails. func (p *Project) Initialize(overwrite bool) error { if p.Workstation != nil && p.Workstation.NetworkManager != nil { - if err := p.Workstation.NetworkManager.Initialize(p.Workstation.Services); err != nil { - return fmt.Errorf("failed to initialize network manager: %w", err) + if err := p.Workstation.NetworkManager.AssignIPs(p.Workstation.Services); err != nil { + return fmt.Errorf("failed to assign IPs to network manager: %w", err) } } diff --git a/pkg/workstation/network/colima_network.go b/pkg/workstation/network/colima_network.go index e10257d13..ad4257658 100644 --- a/pkg/workstation/network/colima_network.go +++ b/pkg/workstation/network/colima_network.go @@ -6,10 +6,9 @@ import ( "strings" "github.com/windsorcli/cli/pkg/constants" - "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/runtime" "github.com/windsorcli/cli/pkg/runtime/shell" "github.com/windsorcli/cli/pkg/runtime/shell/ssh" - "github.com/windsorcli/cli/pkg/workstation/services" ) // The ColimaNetworkManager is a specialized network manager for Colima-based environments. @@ -32,51 +31,20 @@ type ColimaNetworkManager struct { // ============================================================================= // NewColimaNetworkManager creates a new ColimaNetworkManager -func NewColimaNetworkManager(injector di.Injector) *ColimaNetworkManager { +func NewColimaNetworkManager(rt *runtime.Runtime, sshClient ssh.Client, secureShell shell.Shell, networkInterfaceProvider NetworkInterfaceProvider) *ColimaNetworkManager { manager := &ColimaNetworkManager{ - BaseNetworkManager: *NewBaseNetworkManager(injector), - } - if provider, ok := injector.Resolve("networkInterfaceProvider").(NetworkInterfaceProvider); ok { - manager.networkInterfaceProvider = provider - } - return manager -} - -// ============================================================================= -// Public Methods -// ============================================================================= - -// Initialize sets up the ColimaNetworkManager by resolving dependencies for -// sshClient, shell, and secureShell from the injector. -func (n *ColimaNetworkManager) Initialize(services []services.Service) error { - if err := n.BaseNetworkManager.Initialize(services); err != nil { - return err + BaseNetworkManager: *NewBaseNetworkManager(rt), + networkInterfaceProvider: networkInterfaceProvider, } - - sshClient, ok := n.injector.Resolve("sshClient").(ssh.Client) - if !ok { - return fmt.Errorf("resolved ssh client instance is not of type ssh.Client") - } - n.sshClient = sshClient - - secureShell, ok := n.injector.Resolve("secureShell").(shell.Shell) - if !ok { - return fmt.Errorf("resolved secure shell instance is not of type shell.Shell") - } - n.secureShell = secureShell - - networkInterfaceProvider, ok := n.injector.Resolve("networkInterfaceProvider").(NetworkInterfaceProvider) - if !ok { - return fmt.Errorf("failed to resolve network interface provider") - } - n.networkInterfaceProvider = networkInterfaceProvider + manager.sshClient = sshClient + manager.secureShell = secureShell // Set docker.NetworkCIDR to the default value if it's not set - if n.configHandler.GetString("network.cidr_block") == "" { - return n.configHandler.Set("network.cidr_block", constants.DefaultNetworkCIDR) + if manager.configHandler.GetString("network.cidr_block") == "" { + manager.configHandler.Set("network.cidr_block", constants.DefaultNetworkCIDR) } - return nil + return manager } // ConfigureGuest sets up forwarding of guest traffic to the container network. diff --git a/pkg/workstation/network/colima_network_test.go b/pkg/workstation/network/colima_network_test.go index 2960605c2..e15cbe460 100644 --- a/pkg/workstation/network/colima_network_test.go +++ b/pkg/workstation/network/colima_network_test.go @@ -13,69 +13,30 @@ import ( // Test Public Methods // ============================================================================= -func TestColimaNetworkManager_Initialize(t *testing.T) { +func TestColimaNetworkManager_AssignIPs(t *testing.T) { setup := func(t *testing.T) (*ColimaNetworkManager, *Mocks) { t.Helper() mocks := setupMocks(t) - manager := NewColimaNetworkManager(mocks.Injector) + manager := NewColimaNetworkManager(mocks.Runtime, mocks.SSHClient, mocks.SecureShell, mocks.NetworkInterfaceProvider) manager.shims = mocks.Shims return manager, mocks } - t.Run("ErrorResolvingSecureShell", func(t *testing.T) { - // Given a network manager with invalid secure shell - manager, mocks := setup(t) - mocks.Injector.Register("secureShell", "invalid") - - // When initializing the network manager - err := manager.Initialize([]services.Service{}) - - // Then an error should occur - if err == nil { - t.Fatalf("expected an error during Initialize, got nil") - } - - // And the error should be about secure shell type - if err.Error() != "resolved secure shell instance is not of type shell.Shell" { - t.Fatalf("unexpected error message: got %v", err) - } - }) - - t.Run("ErrorResolvingSSHClient", func(t *testing.T) { - // Given a network manager with invalid SSH client - manager, mocks := setup(t) - mocks.Injector.Register("sshClient", "invalid") - - // When initializing the network manager - err := manager.Initialize([]services.Service{}) - - // Then an error should occur - if err == nil { - t.Fatalf("expected an error during Initialize, got nil") - } - - // And the error should be about SSH client type - if err.Error() != "resolved ssh client instance is not of type ssh.Client" { - t.Fatalf("unexpected error message: got %v", err) - } - }) - - t.Run("ErrorResolvingNetworkInterfaceProvider", func(t *testing.T) { - // Given a network manager with invalid network interface provider - manager, mocks := setup(t) - mocks.Injector.Register("networkInterfaceProvider", "invalid") + t.Run("Success", func(t *testing.T) { + // Given a properly configured network manager + manager, _ := setup(t) - // When initializing the network manager - err := manager.Initialize([]services.Service{}) + // When assigning IPs to services + err := manager.AssignIPs([]services.Service{}) - // Then an error should occur - if err == nil { - t.Fatalf("expected an error during Initialize, got nil") + // Then no error should occur + if err != nil { + t.Fatalf("expected no error during AssignIPs, got %v", err) } - // And the error should be about network interface provider type - if err.Error() != "failed to resolve network interface provider" { - t.Fatalf("unexpected error message: got %v", err) + // And services should be set + if manager.services == nil { + t.Fatalf("expected services to be set") } }) } @@ -84,9 +45,9 @@ func TestColimaNetworkManager_ConfigureGuest(t *testing.T) { setup := func(t *testing.T) (*ColimaNetworkManager, *Mocks) { t.Helper() mocks := setupMocks(t) - manager := NewColimaNetworkManager(mocks.Injector) + manager := NewColimaNetworkManager(mocks.Runtime, mocks.SSHClient, mocks.SecureShell, mocks.NetworkInterfaceProvider) manager.shims = mocks.Shims - manager.Initialize([]services.Service{}) + manager.AssignIPs([]services.Service{}) return manager, mocks } @@ -150,7 +111,7 @@ func TestColimaNetworkManager_ConfigureGuest(t *testing.T) { } // When initializing the network manager - err := manager.Initialize([]services.Service{}) + err := manager.AssignIPs([]services.Service{}) if err != nil { t.Fatalf("expected no error during initialization, got %v", err) } @@ -176,7 +137,7 @@ func TestColimaNetworkManager_ConfigureGuest(t *testing.T) { } // When initializing the network manager - err := manager.Initialize([]services.Service{}) + err := manager.AssignIPs([]services.Service{}) if err != nil { t.Fatalf("expected no error during initialization, got %v", err) } @@ -205,7 +166,7 @@ func TestColimaNetworkManager_ConfigureGuest(t *testing.T) { } // When initializing the network manager - err := manager.Initialize([]services.Service{}) + err := manager.AssignIPs([]services.Service{}) if err != nil { t.Fatalf("expected no error during initialization, got %v", err) } @@ -234,7 +195,7 @@ func TestColimaNetworkManager_ConfigureGuest(t *testing.T) { } // When initializing the network manager - err := manager.Initialize([]services.Service{}) + err := manager.AssignIPs([]services.Service{}) if err != nil { t.Fatalf("expected no error during initialization, got %v", err) } @@ -269,7 +230,7 @@ func TestColimaNetworkManager_ConfigureGuest(t *testing.T) { } // When initializing the network manager - err := manager.Initialize([]services.Service{}) + err := manager.AssignIPs([]services.Service{}) if err != nil { t.Fatalf("expected no error during initialization, got %v", err) } @@ -295,7 +256,7 @@ func TestColimaNetworkManager_ConfigureGuest(t *testing.T) { } // When initializing the network manager - err := manager.Initialize([]services.Service{}) + err := manager.AssignIPs([]services.Service{}) if err != nil { t.Fatalf("expected no error during initialization, got %v", err) } @@ -324,7 +285,7 @@ func TestColimaNetworkManager_ConfigureGuest(t *testing.T) { } // When initializing the network manager - err := manager.Initialize([]services.Service{}) + err := manager.AssignIPs([]services.Service{}) if err != nil { t.Fatalf("expected no error during initialization, got %v", err) } @@ -346,8 +307,8 @@ func TestColimaNetworkManager_getHostIP(t *testing.T) { setup := func(t *testing.T) (*ColimaNetworkManager, *Mocks) { t.Helper() mocks := setupMocks(t) - manager := NewColimaNetworkManager(mocks.Injector) - manager.Initialize([]services.Service{}) + manager := NewColimaNetworkManager(mocks.Runtime, mocks.SSHClient, mocks.SecureShell, mocks.NetworkInterfaceProvider) + manager.AssignIPs([]services.Service{}) return manager, mocks } diff --git a/pkg/workstation/network/darwin_network_test.go b/pkg/workstation/network/darwin_network_test.go index e2e5ae72d..32dd8842e 100644 --- a/pkg/workstation/network/darwin_network_test.go +++ b/pkg/workstation/network/darwin_network_test.go @@ -19,9 +19,9 @@ func TestDarwinNetworkManager_ConfigureHostRoute(t *testing.T) { setup := func(t *testing.T) (*BaseNetworkManager, *Mocks) { t.Helper() mocks := setupMocks(t) - manager := NewBaseNetworkManager(mocks.Injector) + manager := NewBaseNetworkManager(mocks.Runtime) manager.shims = mocks.Shims - manager.Initialize([]services.Service{}) + manager.AssignIPs([]services.Service{}) return manager, mocks } @@ -167,9 +167,9 @@ func TestDarwinNetworkManager_ConfigureDNS(t *testing.T) { setup := func(t *testing.T) (*BaseNetworkManager, *Mocks) { t.Helper() mocks := setupMocks(t) - manager := NewBaseNetworkManager(mocks.Injector) + manager := NewBaseNetworkManager(mocks.Runtime) manager.shims = mocks.Shims - manager.Initialize([]services.Service{}) + manager.AssignIPs([]services.Service{}) return manager, mocks } diff --git a/pkg/workstation/network/linux_network_test.go b/pkg/workstation/network/linux_network_test.go index c9478e7e4..4c8ef8fa3 100644 --- a/pkg/workstation/network/linux_network_test.go +++ b/pkg/workstation/network/linux_network_test.go @@ -20,9 +20,9 @@ func TestLinuxNetworkManager_ConfigureHostRoute(t *testing.T) { setup := func(t *testing.T) (*BaseNetworkManager, *Mocks) { t.Helper() mocks := setupMocks(t) - manager := NewBaseNetworkManager(mocks.Injector) + manager := NewBaseNetworkManager(mocks.Runtime) manager.shims = mocks.Shims - manager.Initialize([]services.Service{}) + manager.AssignIPs([]services.Service{}) return manager, mocks } @@ -169,9 +169,9 @@ func TestLinuxNetworkManager_ConfigureDNS(t *testing.T) { setup := func(t *testing.T) (*BaseNetworkManager, *Mocks) { t.Helper() mocks := setupMocks(t) - manager := NewBaseNetworkManager(mocks.Injector) + manager := NewBaseNetworkManager(mocks.Runtime) manager.shims = mocks.Shims - manager.Initialize([]services.Service{}) + manager.AssignIPs([]services.Service{}) return manager, mocks } diff --git a/pkg/workstation/network/mock_network.go b/pkg/workstation/network/mock_network.go index 7f81a4beb..6973f59b1 100644 --- a/pkg/workstation/network/mock_network.go +++ b/pkg/workstation/network/mock_network.go @@ -18,7 +18,7 @@ import ( // MockNetworkManager is a struct that simulates a network manager for testing purposes. type MockNetworkManager struct { NetworkManager - InitializeFunc func([]services.Service) error + AssignIPsFunc func([]services.Service) error ConfigureHostRouteFunc func() error ConfigureGuestFunc func() error ConfigureDNSFunc func() error @@ -37,10 +37,10 @@ func NewMockNetworkManager() *MockNetworkManager { // Public Methods // ============================================================================= -// Initialize calls the custom InitializeFunc if provided. -func (m *MockNetworkManager) Initialize(services []services.Service) error { - if m.InitializeFunc != nil { - return m.InitializeFunc(services) +// AssignIPs calls the custom AssignIPsFunc if provided. +func (m *MockNetworkManager) AssignIPs(services []services.Service) error { + if m.AssignIPsFunc != nil { + return m.AssignIPsFunc(services) } return nil } diff --git a/pkg/workstation/network/mock_network_test.go b/pkg/workstation/network/mock_network_test.go index 6603434cb..646ce38eb 100644 --- a/pkg/workstation/network/mock_network_test.go +++ b/pkg/workstation/network/mock_network_test.go @@ -10,16 +10,16 @@ import ( // Test Public Methods // ============================================================================= -func TestMockNetworkManager_Initialize(t *testing.T) { +func TestMockNetworkManager_AssignIPs(t *testing.T) { t.Run("Success", func(t *testing.T) { - // Given a mock network manager with successful initialization + // Given a mock network manager with successful IP assignment mockManager := NewMockNetworkManager() - mockManager.InitializeFunc = func([]services.Service) error { + mockManager.AssignIPsFunc = func([]services.Service) error { return nil } - // When initializing the manager - err := mockManager.Initialize([]services.Service{}) + // When assigning IPs + err := mockManager.AssignIPs([]services.Service{}) // Then no error should occur if err != nil { @@ -28,11 +28,11 @@ func TestMockNetworkManager_Initialize(t *testing.T) { }) t.Run("NoFuncSet", func(t *testing.T) { - // Given a mock network manager with no initialization function + // Given a mock network manager with no IP assignment function mockManager := NewMockNetworkManager() - // When initializing the manager - err := mockManager.Initialize([]services.Service{}) + // When assigning IPs + err := mockManager.AssignIPs([]services.Service{}) // Then no error should occur if err != nil { diff --git a/pkg/workstation/network/network.go b/pkg/workstation/network/network.go index 4b8b90d0e..321a390f9 100644 --- a/pkg/workstation/network/network.go +++ b/pkg/workstation/network/network.go @@ -6,10 +6,10 @@ import ( "sort" "github.com/windsorcli/cli/pkg/constants" + "github.com/windsorcli/cli/pkg/runtime" "github.com/windsorcli/cli/pkg/runtime/config" "github.com/windsorcli/cli/pkg/runtime/shell" "github.com/windsorcli/cli/pkg/runtime/shell/ssh" - "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/workstation/services" ) @@ -24,7 +24,7 @@ import ( // NetworkManager handles configuring the local development network type NetworkManager interface { - Initialize(services []services.Service) error + AssignIPs(services []services.Service) error ConfigureHostRoute() error ConfigureGuest() error ConfigureDNS() error @@ -32,14 +32,15 @@ type NetworkManager interface { // BaseNetworkManager is a concrete implementation of NetworkManager type BaseNetworkManager struct { - injector di.Injector - sshClient ssh.Client - shell shell.Shell - secureShell shell.Shell - configHandler config.ConfigHandler - services []services.Service - shims *Shims - networkInterfaceProvider NetworkInterfaceProvider + runtime *runtime.Runtime + sshClient ssh.Client + shell shell.Shell + secureShell shell.Shell + configHandler config.ConfigHandler + services []services.Service + shims *Shims + networkInterfaceProvider NetworkInterfaceProvider + portAllocator *services.PortAllocator } // ============================================================================= @@ -47,10 +48,13 @@ type BaseNetworkManager struct { // ============================================================================= // NewNetworkManager creates a new NetworkManager -func NewBaseNetworkManager(injector di.Injector) *BaseNetworkManager { +func NewBaseNetworkManager(rt *runtime.Runtime) *BaseNetworkManager { return &BaseNetworkManager{ - injector: injector, - shims: NewShims(), + runtime: rt, + shell: rt.Shell, + configHandler: rt.ConfigHandler, + shims: NewShims(), + portAllocator: services.NewPortAllocator(), } } @@ -58,21 +62,9 @@ func NewBaseNetworkManager(injector di.Injector) *BaseNetworkManager { // Public Methods // ============================================================================= -// Initialize resolves dependencies, sorts services, and assigns IPs based on network CIDR. +// AssignIPs sorts services and assigns IPs based on network CIDR. // Services are passed explicitly from Workstation to ensure we work with the same instances. -func (n *BaseNetworkManager) Initialize(serviceList []services.Service) error { - shellInterface, ok := n.injector.Resolve("shell").(shell.Shell) - if !ok { - return fmt.Errorf("resolved shell instance is not of type shell.Shell") - } - n.shell = shellInterface - - configHandler, ok := n.injector.Resolve("configHandler").(config.ConfigHandler) - if !ok { - return fmt.Errorf("error resolving configHandler") - } - n.configHandler = configHandler - +func (n *BaseNetworkManager) AssignIPs(serviceList []services.Service) error { // Sort services by name for consistent IP assignment sort.Slice(serviceList, func(i, j int) bool { return serviceList[i].GetName() < serviceList[j].GetName() @@ -80,9 +72,6 @@ func (n *BaseNetworkManager) Initialize(serviceList []services.Service) error { n.services = serviceList - // Create PortAllocator for this initialization run - portAllocator := services.NewPortAllocator() - networkCIDR := n.configHandler.GetString("network.cidr_block") if networkCIDR == "" { networkCIDR = constants.DefaultNetworkCIDR @@ -90,7 +79,7 @@ func (n *BaseNetworkManager) Initialize(serviceList []services.Service) error { return fmt.Errorf("error setting default network CIDR: %w", err) } } - if err := assignIPAddresses(n.services, &networkCIDR, portAllocator); err != nil { + if err := assignIPAddresses(n.services, &networkCIDR, n.portAllocator); err != nil { return fmt.Errorf("error assigning IP addresses: %w", err) } diff --git a/pkg/workstation/network/network_test.go b/pkg/workstation/network/network_test.go index 990b90de2..46a98695f 100644 --- a/pkg/workstation/network/network_test.go +++ b/pkg/workstation/network/network_test.go @@ -7,10 +7,11 @@ import ( "strings" "testing" + "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/runtime" "github.com/windsorcli/cli/pkg/runtime/config" "github.com/windsorcli/cli/pkg/runtime/shell" "github.com/windsorcli/cli/pkg/runtime/shell/ssh" - "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/workstation/services" ) @@ -19,18 +20,17 @@ import ( // ============================================================================= type Mocks struct { - Injector di.Injector - ConfigHandler config.ConfigHandler - Shell *shell.MockShell - SecureShell *shell.MockShell - SSHClient *ssh.MockClient - NetworkInterfaceProvider *MockNetworkInterfaceProvider - Services []*services.MockService - Shims *Shims + Runtime *runtime.Runtime + ConfigHandler config.ConfigHandler + Shell *shell.MockShell + SecureShell *shell.MockShell + SSHClient *ssh.MockClient + NetworkInterfaceProvider *MockNetworkInterfaceProvider + Services []*services.MockService + Shims *Shims } type SetupOptions struct { - Injector di.Injector ConfigHandler config.ConfigHandler ConfigStr string } @@ -72,12 +72,10 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { } }) - // Create injector if not provided - var injector di.Injector - if len(opts) > 0 && opts[0].Injector != nil { - injector = opts[0].Injector - } else { - injector = di.NewInjector() + // Create a mock shell first (needed for config handler) + mockShell := shell.NewMockShell(nil) + mockShell.GetProjectRootFunc = func() (string, error) { + return tmpDir, nil } // Create config handler if not provided @@ -85,9 +83,14 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { if len(opts) > 0 && opts[0].ConfigHandler != nil { configHandler = opts[0].ConfigHandler } else { + // Create minimal injector for config handler initialization + injector := di.NewInjector() + injector.Register("shell", mockShell) configHandler = config.NewConfigHandler(injector) + if err := configHandler.Initialize(); err != nil { + t.Fatalf("Failed to initialize config handler: %v", err) + } } - injector.Register("configHandler", configHandler) configYAML := ` version: v1alpha1 @@ -112,8 +115,7 @@ contexts: } } - // Create a mock shell - mockShell := shell.NewMockShell(injector) + // Configure mock shell functions mockShell.ExecFunc = func(command string, args ...string) (string, error) { return "", nil } @@ -126,10 +128,9 @@ contexts: } return "", nil } - injector.Register("shell", mockShell) // Create a mock secure shell - mockSecureShell := shell.NewMockShell(injector) + mockSecureShell := shell.NewMockShell(nil) mockSecureShell.ExecFunc = func(command string, args ...string) (string, error) { return "", nil } @@ -142,11 +143,9 @@ contexts: } return "", nil } - injector.Register("secureShell", mockSecureShell) // Create a mock SSH client mockSSHClient := ssh.NewMockSSHClient() - injector.Register("sshClient", mockSSHClient) // Create a mock network interface provider with mock functions mockNetworkInterfaceProvider := &MockNetworkInterfaceProvider{ @@ -185,17 +184,23 @@ contexts: } }, } - injector.Register("networkInterfaceProvider", mockNetworkInterfaceProvider) // Create mock services mockService1 := services.NewMockService() mockService2 := services.NewMockService() - injector.Register("service1", mockService1) - injector.Register("service2", mockService2) + + rt := &runtime.Runtime{ + ProjectRoot: tmpDir, + ConfigRoot: tmpDir, + TemplateRoot: tmpDir, + ContextName: "mock-context", + ConfigHandler: configHandler, + Shell: mockShell, + } // Create mocks struct with references to the same instances mocks := &Mocks{ - Injector: injector, + Runtime: rt, ConfigHandler: configHandler, Shell: mockShell, SecureShell: mockSecureShell, @@ -217,11 +222,14 @@ contexts: func TestNetworkManager_NewNetworkManager(t *testing.T) { t.Run("Success", func(t *testing.T) { - // Given a DI container - injector := di.NewInjector() + // Given a runtime + rt := &runtime.Runtime{ + ConfigHandler: config.NewMockConfigHandler(), + Shell: shell.NewMockShell(nil), + } // When creating a new BaseNetworkManager - nm := NewBaseNetworkManager(injector) + nm := NewBaseNetworkManager(rt) // Then the NetworkManager should not be nil if nm == nil { @@ -234,11 +242,11 @@ func TestNetworkManager_NewNetworkManager(t *testing.T) { // Test Public Methods // ============================================================================= -func TestNetworkManager_Initialize(t *testing.T) { +func TestNetworkManager_AssignIPs(t *testing.T) { setup := func(t *testing.T) (*BaseNetworkManager, *Mocks) { t.Helper() mocks := setupMocks(t) - manager := NewBaseNetworkManager(mocks.Injector) + manager := NewBaseNetworkManager(mocks.Runtime) manager.shims = mocks.Shims return manager, mocks } @@ -266,8 +274,8 @@ func TestNetworkManager_Initialize(t *testing.T) { serviceList[i] = s } - // When creating and initializing the network manager - err := manager.Initialize(serviceList) + // When assigning IPs to services + err := manager.AssignIPs(serviceList) // Then no error should occur if err != nil { @@ -302,8 +310,8 @@ func TestNetworkManager_Initialize(t *testing.T) { serviceList[i] = s } - // When initializing the network manager - err := manager.Initialize(serviceList) + // When assigning IPs + err := manager.AssignIPs(serviceList) // Then an error should occur if err == nil { @@ -317,51 +325,14 @@ func TestNetworkManager_Initialize(t *testing.T) { } }) - t.Run("ErrorResolvingShell", func(t *testing.T) { - // Given a network manager with invalid shell - manager, mocks := setup(t) - mocks.Injector.Register("shell", "invalid") - - // When initializing the network manager - err := manager.Initialize([]services.Service{}) - - // Then an error should occur - if err == nil { - t.Fatalf("expected an error during Initialize, got nil") - } - - // And the error should be about shell type - if err.Error() != "resolved shell instance is not of type shell.Shell" { - t.Fatalf("unexpected error message: got %v", err) - } - }) - - t.Run("ErrorResolvingConfigHandler", func(t *testing.T) { - // Given a network manager with invalid config handler - manager, mocks := setup(t) - mocks.Injector.Register("configHandler", "invalid") - - // When initializing the network manager - err := manager.Initialize([]services.Service{}) - - // Then an error should occur - if err == nil { - t.Fatalf("expected an error during Initialize, got nil") - } - - // And the error should be about config handler - if err.Error() != "error resolving configHandler" { - t.Fatalf("unexpected error message: got %v", err) - } - }) t.Run("ErrorResolvingServices", func(t *testing.T) { // Given a network manager manager, mocks := setup(t) - // When initializing the network manager with services + // When assigning IPs with services // (services are now passed explicitly, so no resolution error can occur) - err := manager.Initialize([]services.Service{}) + err := manager.AssignIPs([]services.Service{}) // Then no error should occur (services are passed directly, not resolved) if err != nil { @@ -399,11 +370,11 @@ func TestNetworkManager_Initialize(t *testing.T) { mocks := setupMocks(t, &SetupOptions{ ConfigHandler: mockConfigHandler, }) - manager := NewBaseNetworkManager(mocks.Injector) + manager := NewBaseNetworkManager(mocks.Runtime) manager.shims = mocks.Shims - // When initializing the network manager - err := manager.Initialize([]services.Service{}) + // When assigning IPs + err := manager.AssignIPs([]services.Service{}) // Then an error should occur if err == nil { @@ -427,8 +398,8 @@ func TestNetworkManager_Initialize(t *testing.T) { } serviceList := []services.Service{mockService} - // When initializing the network manager - err := manager.Initialize(serviceList) + // When assigning IPs + err := manager.AssignIPs(serviceList) // Then an error should occur if err == nil { @@ -443,32 +414,13 @@ func TestNetworkManager_Initialize(t *testing.T) { _ = mocks // suppress unused variable warning }) - t.Run("ResolveShellFailure", func(t *testing.T) { - // Given a network manager with shell resolution failure - manager, mocks := setup(t) - mocks.Injector.Register("shell", "invalid") - - // When initializing the network manager - err := manager.Initialize([]services.Service{}) - - // Then an error should occur - if err == nil { - t.Fatalf("expected error during Initialize, got nil") - } - - // And the error should contain the expected message - expectedErrorSubstring := "resolved shell instance is not of type shell.Shell" - if !strings.Contains(err.Error(), expectedErrorSubstring) { - t.Errorf("expected error message to contain %q, got %q", expectedErrorSubstring, err.Error()) - } - }) } func TestNetworkManager_ConfigureGuest(t *testing.T) { setup := func(t *testing.T) (*BaseNetworkManager, *Mocks) { t.Helper() mocks := setupMocks(t) - manager := NewBaseNetworkManager(mocks.Injector) + manager := NewBaseNetworkManager(mocks.Runtime) manager.shims = mocks.Shims return manager, mocks } @@ -491,7 +443,7 @@ func TestNetworkManager_assignIPAddresses(t *testing.T) { setup := func(t *testing.T) (*BaseNetworkManager, *Mocks) { t.Helper() mocks := setupMocks(t) - manager := NewBaseNetworkManager(mocks.Injector) + manager := NewBaseNetworkManager(mocks.Runtime) manager.shims = mocks.Shims return manager, mocks } diff --git a/pkg/workstation/network/windows_network_test.go b/pkg/workstation/network/windows_network_test.go index 0ce006a1f..6b45d8f48 100644 --- a/pkg/workstation/network/windows_network_test.go +++ b/pkg/workstation/network/windows_network_test.go @@ -19,9 +19,9 @@ func TestWindowsNetworkManager_ConfigureHostRoute(t *testing.T) { setup := func(t *testing.T) (*BaseNetworkManager, *Mocks) { t.Helper() mocks := setupMocks(t) - manager := NewBaseNetworkManager(mocks.Injector) + manager := NewBaseNetworkManager(mocks.Runtime) manager.shims = mocks.Shims - manager.Initialize([]services.Service{}) + manager.AssignIPs([]services.Service{}) return manager, mocks } @@ -142,9 +142,9 @@ func TestWindowsNetworkManager_ConfigureDNS(t *testing.T) { setup := func(t *testing.T) (*BaseNetworkManager, *Mocks) { t.Helper() mocks := setupMocks(t) - manager := NewBaseNetworkManager(mocks.Injector) + manager := NewBaseNetworkManager(mocks.Runtime) manager.shims = mocks.Shims - manager.Initialize([]services.Service{}) + manager.AssignIPs([]services.Service{}) return manager, mocks } diff --git a/pkg/workstation/services/dns_service.go b/pkg/workstation/services/dns_service.go index ba31c66c7..9df6d5686 100644 --- a/pkg/workstation/services/dns_service.go +++ b/pkg/workstation/services/dns_service.go @@ -7,7 +7,7 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/windsorcli/cli/pkg/constants" - "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/runtime" ) // The DNSService is a core component that manages DNS configuration and resolution @@ -30,9 +30,9 @@ type DNSService struct { // ============================================================================= // NewDNSService creates a new DNSService -func NewDNSService(injector di.Injector) *DNSService { +func NewDNSService(rt *runtime.Runtime) *DNSService { return &DNSService{ - BaseService: *NewBaseService(injector), + BaseService: *NewBaseService(rt), } } @@ -40,20 +40,9 @@ func NewDNSService(injector di.Injector) *DNSService { // Public Methods // ============================================================================= -// Initialize sets up DNSService by resolving dependencies via DI. -func (s *DNSService) Initialize() error { - if err := s.BaseService.Initialize(); err != nil { - return err - } - resolvedServices, err := s.injector.ResolveAll(new(Service)) - if err != nil { - return fmt.Errorf("error resolving services: %w", err) - } - for _, serviceInterface := range resolvedServices { - service, _ := serviceInterface.(Service) - s.services = append(s.services, service) - } - return nil +// SetServices sets the services list for DNS service +func (s *DNSService) SetServices(services []Service) { + s.services = services } // SetAddress updates DNS address in config and calls BaseService's SetAddress. @@ -115,10 +104,7 @@ func (s *DNSService) GetComposeConfig() (*types.Config, error) { // In localhost mode, it uses a template for local DNS resolution and sets up forwarding rules for DNS queries. // The generated Corefile is saved in the .windsor directory for CoreDNS to manage project DNS queries. func (s *DNSService) WriteConfig() error { - projectRoot, err := s.shell.GetProjectRoot() - if err != nil { - return fmt.Errorf("error retrieving project root: %w", err) - } + projectRoot := s.runtime.ProjectRoot tld := s.configHandler.GetString("dns.domain", "test") diff --git a/pkg/workstation/services/dns_service_test.go b/pkg/workstation/services/dns_service_test.go index 9cbd6bfdf..f94a61a29 100644 --- a/pkg/workstation/services/dns_service_test.go +++ b/pkg/workstation/services/dns_service_test.go @@ -8,7 +8,6 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/windsorcli/cli/pkg/runtime/config" - "github.com/windsorcli/cli/pkg/di" ) // ============================================================================= @@ -22,11 +21,6 @@ func setupDnsMocks(t *testing.T, opts ...*SetupOptions) *Mocks { // Create base mocks using setupMocks mocks := setupMocks(t, opts...) - // Create a generic mock service - mockService := NewMockService() - mockService.Initialize() - mocks.Injector.Register("dockerService", mockService) - // Set up shell project root mocks.Shell.GetProjectRootFunc = func() (string, error) { return "/mock/project/root", nil @@ -43,7 +37,7 @@ func TestNewDNSService(t *testing.T) { setup := func(t *testing.T) (*DNSService, *Mocks) { t.Helper() mocks := setupDnsMocks(t) - service := NewDNSService(mocks.Injector) + service := NewDNSService(mocks.Runtime) service.shims = mocks.Shims return service, mocks @@ -68,7 +62,7 @@ func TestDNSService_Initialize(t *testing.T) { setup := func(t *testing.T) (*DNSService, *Mocks) { t.Helper() mocks := setupDnsMocks(t) - service := NewDNSService(mocks.Injector) + service := NewDNSService(mocks.Runtime) service.shims = mocks.Shims return service, mocks @@ -78,59 +72,9 @@ func TestDNSService_Initialize(t *testing.T) { // Given a DNSService with mock components service, _ := setup(t) - // When Initialize is called - err := service.Initialize() - - // Then no error should be returned - if err != nil { - t.Fatalf("Initialize() error = %v", err) - } - }) - - t.Run("ErrorResolvingConfigHandler", func(t *testing.T) { - // Given a DNSService with mock components - service, mocks := setup(t) - - // And the configHandler is registered as invalid - mocks.Injector.Register("configHandler", "invalid") - - // When Initialize is called - err := service.Initialize() - - // Then an error should be returned with the expected message - if err == nil { - t.Fatalf("Expected error resolving configHandler, got nil") - } - expectedErrorMessage := "error resolving configHandler" - if err.Error() != expectedErrorMessage { - t.Errorf("Expected error message '%s', got %v", expectedErrorMessage, err) - } - }) - - t.Run("ErrorResolvingServices", func(t *testing.T) { - // Given a mock injector - mockInjector := di.NewMockInjector() - - // And the injector is configured to return an error for services - mockInjector.SetResolveAllError(new(Service), fmt.Errorf("error resolving services")) - - // And a DNSService with the mock injector - mocks := setupDnsMocks(t, &SetupOptions{ - Injector: mockInjector, - }) - service := NewDNSService(mocks.Injector) - service.shims = mocks.Shims - - // When Initialize is called - err := service.Initialize() - - // Then an error should be returned with the expected message - if err == nil { - t.Fatalf("Expected error resolving services, got nil") - } - expectedErrorMessage := "error resolving services: error resolving services" - if err.Error() != expectedErrorMessage { - t.Errorf("Expected error message '%s', got %v", expectedErrorMessage, err) + // Then the service should be properly initialized + if service == nil { + t.Fatalf("service should not be nil") } }) } @@ -139,9 +83,8 @@ func TestDNSService_SetAddress(t *testing.T) { setup := func(t *testing.T) (*DNSService, *Mocks) { t.Helper() mocks := setupDnsMocks(t) - service := NewDNSService(mocks.Injector) + service := NewDNSService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() return service, mocks } @@ -174,9 +117,8 @@ func TestDNSService_SetAddress(t *testing.T) { mocks := setupDnsMocks(t, &SetupOptions{ ConfigHandler: mockConfigHandler, }) - service := NewDNSService(mocks.Injector) + service := NewDNSService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() // When SetAddress is called address := "127.0.0.1" @@ -197,10 +139,9 @@ func TestDNSService_GetComposeConfig(t *testing.T) { setup := func(t *testing.T) (*DNSService, *Mocks) { t.Helper() mocks := setupDnsMocks(t) - service := NewDNSService(mocks.Injector) + service := NewDNSService(mocks.Runtime) service.SetName("dns") service.shims = mocks.Shims - service.Initialize() return service, mocks } @@ -278,9 +219,8 @@ func TestDNSService_WriteConfig(t *testing.T) { setup := func(t *testing.T) (*DNSService, *Mocks) { t.Helper() mocks := setupDnsMocks(t) - service := NewDNSService(mocks.Injector) + service := NewDNSService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() return service, mocks } @@ -426,8 +366,7 @@ func TestDNSService_WriteConfig(t *testing.T) { return false } - // Register the mock service - mocks.Injector.Register("test-service", mockService) + // Set the mock service directly service.services = []Service{mockService} // Mock the writeFile function to capture the content written @@ -479,8 +418,7 @@ func TestDNSService_WriteConfig(t *testing.T) { return true } - // Register the mock service - mocks.Injector.Register("test-service", mockService) + // Set the mock service directly service.services = []Service{mockService} // Mock the writeFile function to capture the content written @@ -555,8 +493,6 @@ func TestDNSService_WriteConfig(t *testing.T) { } // Register the mock services - mocks.Injector.Register("test-service-no-name", mockServiceNoName) - mocks.Injector.Register("test-service-no-address", mockServiceNoAddress) service.services = []Service{mockServiceNoName, mockServiceNoAddress} // Mock the writeFile function to capture the content written @@ -659,28 +595,6 @@ func TestDNSService_WriteConfig(t *testing.T) { } }) - t.Run("ErrorRetrievingProjectRoot", func(t *testing.T) { - // Given a DNSService with mock components - service, mocks := setup(t) - - // Set up mock to fail when getting project root - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("error getting project root") - } - - // When WriteConfig is called - err := service.WriteConfig() - - // Then an error should be returned - if err == nil { - t.Error("WriteConfig() expected error, got nil") - } - - // And the error should contain the expected message - if !strings.Contains(err.Error(), "error retrieving project root") { - t.Errorf("Expected error to contain 'error retrieving project root', got: %v", err) - } - }) t.Run("SuccessRemovingCorefileDirectory", func(t *testing.T) { // Given a DNSService with mock components @@ -731,9 +645,8 @@ func TestDNSService_SetName(t *testing.T) { setup := func(t *testing.T) (*DNSService, *Mocks) { t.Helper() mocks := setupDnsMocks(t) - service := NewDNSService(mocks.Injector) + service := NewDNSService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() return service, mocks } @@ -757,9 +670,8 @@ func TestDNSService_GetName(t *testing.T) { setupSuccess := func(t *testing.T) (*DNSService, *Mocks) { t.Helper() mocks := setupDnsMocks(t) - service := NewDNSService(mocks.Injector) + service := NewDNSService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() service.SetName("dns") // Set the name to "dns" return service, mocks @@ -768,9 +680,8 @@ func TestDNSService_GetName(t *testing.T) { setupError := func(t *testing.T) (*DNSService, *Mocks) { t.Helper() mocks := setupDnsMocks(t) - service := NewDNSService(mocks.Injector) + service := NewDNSService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() // Don't set the name return service, mocks @@ -812,9 +723,8 @@ func TestDNSService_GetHostname(t *testing.T) { setup := func(t *testing.T) (*DNSService, *Mocks) { t.Helper() mocks := setupDnsMocks(t) - service := NewDNSService(mocks.Injector) + service := NewDNSService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() service.SetName("test") // Set the dns.domain configuration value @@ -857,9 +767,8 @@ func TestDNSService_SupportsWildcard(t *testing.T) { setup := func(t *testing.T) (*DNSService, *Mocks) { t.Helper() mocks := setupDnsMocks(t) - service := NewDNSService(mocks.Injector) + service := NewDNSService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() service.SetName("test") return service, mocks diff --git a/pkg/workstation/services/git_livereload_service.go b/pkg/workstation/services/git_livereload_service.go index 78bad2f4b..546a72c43 100644 --- a/pkg/workstation/services/git_livereload_service.go +++ b/pkg/workstation/services/git_livereload_service.go @@ -6,7 +6,7 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/windsorcli/cli/pkg/constants" - "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/runtime" ) // The GitLivereloadService is a service component that manages Git repository synchronization @@ -28,9 +28,9 @@ type GitLivereloadService struct { // ============================================================================= // NewGitLivereloadService is a constructor for GitLivereloadService -func NewGitLivereloadService(injector di.Injector) *GitLivereloadService { +func NewGitLivereloadService(rt *runtime.Runtime) *GitLivereloadService { return &GitLivereloadService{ - BaseService: *NewBaseService(injector), + BaseService: *NewBaseService(rt), } } @@ -66,10 +66,7 @@ func (s *GitLivereloadService) GetComposeConfig() (*types.Config, error) { envVars["WEBHOOK_URL"] = ptrString(webhookUrl) } - projectRoot, err := s.shell.GetProjectRoot() - if err != nil { - return nil, fmt.Errorf("error retrieving project root: %w", err) - } + projectRoot := s.runtime.ProjectRoot gitFolderName := filepath.Base(projectRoot) serviceName := s.name diff --git a/pkg/workstation/services/git_livereload_service_test.go b/pkg/workstation/services/git_livereload_service_test.go index 6f6720ca3..2579ddb27 100644 --- a/pkg/workstation/services/git_livereload_service_test.go +++ b/pkg/workstation/services/git_livereload_service_test.go @@ -1,8 +1,6 @@ package services import ( - "fmt" - "strings" "testing" "github.com/windsorcli/cli/pkg/constants" @@ -23,17 +21,13 @@ func TestGitLivereloadService_NewGitLivereloadService(t *testing.T) { mocks := setupMocks(t) // When a new GitLivereloadService is created - gitLivereloadService := NewGitLivereloadService(mocks.Injector) + gitLivereloadService := NewGitLivereloadService(mocks.Runtime) // Then the GitService should not be nil if gitLivereloadService == nil { t.Fatalf("expected GitLivereloadService, got nil") } - // And the GitService should have the correct injector - if gitLivereloadService.injector != mocks.Injector { - t.Errorf("expected injector %v, got %v", mocks.Injector, gitLivereloadService.injector) - } }) } @@ -41,11 +35,7 @@ func TestGitLivereloadService_GetComposeConfig(t *testing.T) { t.Run("Success", func(t *testing.T) { // Given a mock config handler, shell, context, and service mocks := setupMocks(t) - gitLivereloadService := NewGitLivereloadService(mocks.Injector) - err := gitLivereloadService.Initialize() - if err != nil { - t.Fatalf("Initialize() error = %v", err) - } + gitLivereloadService := NewGitLivereloadService(mocks.Runtime) // Set the service name gitLivereloadService.SetName("git") @@ -73,40 +63,11 @@ func TestGitLivereloadService_GetComposeConfig(t *testing.T) { } }) - t.Run("ErrorGettingProjectRoot", func(t *testing.T) { - // Given a mock config handler with error on GetProjectRoot - mocks := setupMocks(t) - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("mock error retrieving project root") - } - - // And a new GitService is created and initialized - gitLivereloadService := NewGitLivereloadService(mocks.Injector) - err := gitLivereloadService.Initialize() - if err != nil { - t.Fatalf("Initialize() error = %v", err) - } - - // When GetComposeConfig is called - composeConfig, err := gitLivereloadService.GetComposeConfig() - - // Then verify the configuration is empty and an error should be returned - if composeConfig != nil && len(composeConfig.Services) > 0 { - t.Errorf("expected empty configuration, got %+v", composeConfig) - } - if err == nil || !strings.Contains(err.Error(), "mock error retrieving project root") { - t.Fatalf("expected error retrieving project root, got %v", err) - } - }) t.Run("SuccessWithRsyncInclude", func(t *testing.T) { // Given a mock config handler, shell, context, and service with rsync_include configured mocks := setupMocks(t) - gitLivereloadService := NewGitLivereloadService(mocks.Injector) - err := gitLivereloadService.Initialize() - if err != nil { - t.Fatalf("Initialize() error = %v", err) - } + gitLivereloadService := NewGitLivereloadService(mocks.Runtime) // Set the service name gitLivereloadService.SetName("git") @@ -149,11 +110,7 @@ func TestGitLivereloadService_GetComposeConfig(t *testing.T) { t.Run("SuccessWithoutRsyncInclude", func(t *testing.T) { // Given a mock config handler, shell, context, and service without rsync_include configured mocks := setupMocks(t) - gitLivereloadService := NewGitLivereloadService(mocks.Injector) - err := gitLivereloadService.Initialize() - if err != nil { - t.Fatalf("Initialize() error = %v", err) - } + gitLivereloadService := NewGitLivereloadService(mocks.Runtime) // Set the service name gitLivereloadService.SetName("git") diff --git a/pkg/workstation/services/localstack_service.go b/pkg/workstation/services/localstack_service.go index 1f2628940..58ee5cd29 100644 --- a/pkg/workstation/services/localstack_service.go +++ b/pkg/workstation/services/localstack_service.go @@ -6,7 +6,7 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/windsorcli/cli/pkg/constants" - "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/runtime" ) // The LocalstackService is a service component that manages AWS Localstack integration @@ -28,9 +28,9 @@ type LocalstackService struct { // ============================================================================= // NewLocalstackService is a constructor for LocalstackService -func NewLocalstackService(injector di.Injector) *LocalstackService { +func NewLocalstackService(rt *runtime.Runtime) *LocalstackService { return &LocalstackService{ - BaseService: *NewBaseService(injector), + BaseService: *NewBaseService(rt), } } @@ -54,7 +54,7 @@ func (s *LocalstackService) GetComposeConfig() (*types.Config, error) { // Get the localstack services to enable servicesList := "" - if contextConfig.AWS.Localstack.Services != nil { + if contextConfig != nil && contextConfig.AWS != nil && contextConfig.AWS.Localstack != nil && contextConfig.AWS.Localstack.Services != nil { servicesList = strings.Join(contextConfig.AWS.Localstack.Services, ",") } diff --git a/pkg/workstation/services/localstack_service_test.go b/pkg/workstation/services/localstack_service_test.go index dfe53afd8..64a07e32b 100644 --- a/pkg/workstation/services/localstack_service_test.go +++ b/pkg/workstation/services/localstack_service_test.go @@ -13,9 +13,8 @@ func TestLocalstackService_GetComposeConfig(t *testing.T) { setup := func(t *testing.T) (*LocalstackService, *Mocks) { t.Helper() mocks := setupMocks(t) - service := NewLocalstackService(mocks.Injector) + service := NewLocalstackService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() service.SetName("aws") return service, mocks @@ -35,10 +34,6 @@ func TestLocalstackService_GetComposeConfig(t *testing.T) { t.Fatalf("failed to set localstack services: %v", err) } - // Initialize the service - if err := service.Initialize(); err != nil { - t.Fatalf("Initialize() error = %v", err) - } // When: GetComposeConfig is called composeConfig, err := service.GetComposeConfig() @@ -80,10 +75,6 @@ func TestLocalstackService_GetComposeConfig(t *testing.T) { t.Fatalf("failed to set localstack services: %v", err) } - // Initialize the service - if err := service.Initialize(); err != nil { - t.Fatalf("Initialize() error = %v", err) - } // When: GetComposeConfig is called composeConfig, err := service.GetComposeConfig() @@ -111,9 +102,8 @@ func TestLocalstackService_SupportsWildcard(t *testing.T) { setup := func(t *testing.T) (*LocalstackService, *Mocks) { t.Helper() mocks := setupMocks(t) - service := NewLocalstackService(mocks.Injector) + service := NewLocalstackService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() service.SetName("aws") return service, mocks diff --git a/pkg/workstation/services/registry_service.go b/pkg/workstation/services/registry_service.go index aa8958e6d..78b2fd444 100644 --- a/pkg/workstation/services/registry_service.go +++ b/pkg/workstation/services/registry_service.go @@ -9,7 +9,7 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/windsorcli/cli/api/v1alpha1/docker" "github.com/windsorcli/cli/pkg/constants" - "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/runtime" ) // The RegistryService is a service component that manages Docker registry integration @@ -38,9 +38,9 @@ type RegistryService struct { // ============================================================================= // NewRegistryService is a constructor for RegistryService -func NewRegistryService(injector di.Injector) *RegistryService { +func NewRegistryService(rt *runtime.Runtime) *RegistryService { return &RegistryService{ - BaseService: *NewBaseService(injector), + BaseService: *NewBaseService(rt), } } @@ -157,10 +157,7 @@ func (s *RegistryService) generateRegistryService(registry docker.RegistryConfig // Always set environment, even if empty service.Environment = env - projectRoot, err := s.shell.GetProjectRoot() - if err != nil { - return types.ServiceConfig{}, fmt.Errorf("error retrieving project root: %w", err) - } + projectRoot := s.runtime.ProjectRoot cacheDir := projectRoot + "/.windsor/.docker-cache" if err := s.shims.MkdirAll(cacheDir, os.ModePerm); err != nil { return service, fmt.Errorf("error creating .docker-cache directory: %w", err) diff --git a/pkg/workstation/services/registry_service_test.go b/pkg/workstation/services/registry_service_test.go index 84ffdf334..945dde2e7 100644 --- a/pkg/workstation/services/registry_service_test.go +++ b/pkg/workstation/services/registry_service_test.go @@ -21,25 +21,19 @@ func TestRegistryService_NewRegistryService(t *testing.T) { setup := func(t *testing.T) (*RegistryService, *Mocks) { t.Helper() mocks := setupMocks(t) - service := NewRegistryService(mocks.Injector) + service := NewRegistryService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() return service, mocks } t.Run("Success", func(t *testing.T) { // Given a set of mock components - service, mocks := setup(t) + service, _ := setup(t) // Then the RegistryService should not be nil if service == nil { t.Fatalf("expected RegistryService, got nil") } - - // And: the RegistryService should have the correct injector - if service.injector != mocks.Injector { - t.Errorf("expected injector %v, got %v", mocks.Injector, service.injector) - } }) } @@ -51,9 +45,8 @@ func TestRegistryService_GetComposeConfig(t *testing.T) { setup := func(t *testing.T) (*RegistryService, *Mocks) { t.Helper() mocks := setupMocks(t) - service := NewRegistryService(mocks.Injector) + service := NewRegistryService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() service.SetName("registry") return service, mocks } @@ -129,24 +122,6 @@ func TestRegistryService_GetComposeConfig(t *testing.T) { } }) - t.Run("ProjectRootRetrievalFailure", func(t *testing.T) { - // Given a mock config handler, shell, context, and service - service, mocks := setup(t) - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("mock error getting project root") - } - - // When a new RegistryService is created and initialized - service.SetName("registry") - - // When GetComposeConfig is called - _, err := service.GetComposeConfig() - - // Then an error should be returned indicating project root retrieval failure - if err == nil || !strings.Contains(err.Error(), "mock error getting project root") { - t.Fatalf("expected error indicating project root retrieval failure, got %v", err) - } - }) t.Run("LocalRegistry", func(t *testing.T) { // Given a mock config handler, shell, context, and service @@ -226,9 +201,8 @@ contexts: t.Fatalf("Config not loaded correctly, dns.domain = '%s', expected 'test'", domain) } - service := NewRegistryService(mocks.Injector) + service := NewRegistryService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() service.SetName("registry") // Reset package-level variables registryNextPort = constants.RegistryDefaultHostPort + 1 @@ -372,9 +346,8 @@ contexts: } // Create second registry - service2 := NewRegistryService(mocks.Injector) + service2 := NewRegistryService(mocks.Runtime) service2.shims = mocks.Shims - service2.Initialize() service2.SetName("registry2") // When SetAddress is called for second registry @@ -405,9 +378,8 @@ contexts: ConfigHandler: mockConfigHandler, }) - service := NewRegistryService(mocks.Injector) + service := NewRegistryService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() service.SetName("registry") // When SetAddress is called with invalid address @@ -429,9 +401,8 @@ contexts: ConfigHandler: mockConfigHandler, }) - service := NewRegistryService(mocks.Injector) + service := NewRegistryService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() service.SetName("registry") // And mock error when setting hostname @@ -458,9 +429,8 @@ contexts: ConfigHandler: mockConfigHandler, }) - service := NewRegistryService(mocks.Injector) + service := NewRegistryService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() service.SetName("registry") // And mock configuration @@ -510,9 +480,8 @@ contexts: ConfigHandler: mockConfigHandler, }) - service := NewRegistryService(mocks.Injector) + service := NewRegistryService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() service.SetName("registry") // Reset package-level variables @@ -572,8 +541,7 @@ func TestRegistryService_GetHostname(t *testing.T) { mocks := setupMocks(t, &SetupOptions{ ConfigHandler: mockConfigHandler, }) - service := NewRegistryService(mocks.Injector) - service.Initialize() + service := NewRegistryService(mocks.Runtime) return service, mocks } @@ -649,8 +617,7 @@ func TestRegistryService_GetContainerName(t *testing.T) { mocks := setupMocks(t, &SetupOptions{ ConfigHandler: mockConfigHandler, }) - service := NewRegistryService(mocks.Injector) - service.Initialize() + service := NewRegistryService(mocks.Runtime) return service, mocks } @@ -674,9 +641,8 @@ func TestRegistryService_GetName(t *testing.T) { setup := func(t *testing.T) (*RegistryService, *Mocks) { t.Helper() mocks := setupMocks(t) - service := NewRegistryService(mocks.Injector) + service := NewRegistryService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() service.SetName("registry") return service, mocks } @@ -695,9 +661,8 @@ func TestRegistryService_SupportsWildcard(t *testing.T) { setup := func(t *testing.T) (*RegistryService, *Mocks) { t.Helper() mocks := setupMocks(t) - service := NewRegistryService(mocks.Injector) + service := NewRegistryService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() service.SetName("registry") return service, mocks } diff --git a/pkg/workstation/services/service.go b/pkg/workstation/services/service.go index afde7b6f2..adf605957 100644 --- a/pkg/workstation/services/service.go +++ b/pkg/workstation/services/service.go @@ -2,13 +2,12 @@ package services import ( "errors" - "fmt" "net" "strings" "github.com/compose-spec/compose-go/v2/types" + "github.com/windsorcli/cli/pkg/runtime" "github.com/windsorcli/cli/pkg/runtime/config" - "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/runtime/shell" ) @@ -30,7 +29,6 @@ type Service interface { GetAddress() string SetName(name string) GetName() string - Initialize() error SupportsWildcard() bool GetHostname() string } @@ -41,7 +39,7 @@ type Service interface { // BaseService is a base implementation of the Service interface type BaseService struct { - injector di.Injector + runtime *runtime.Runtime configHandler config.ConfigHandler shell shell.Shell address string @@ -54,33 +52,15 @@ type BaseService struct { // ============================================================================= // NewBaseService is a constructor for BaseService -func NewBaseService(injector di.Injector) *BaseService { +func NewBaseService(rt *runtime.Runtime) *BaseService { return &BaseService{ - injector: injector, - shims: NewShims(), + runtime: rt, + configHandler: rt.ConfigHandler, + shell: rt.Shell, + shims: NewShims(), } } -// ============================================================================= -// Public Methods -// ============================================================================= - -func (s *BaseService) Initialize() error { - configHandler, ok := s.injector.Resolve("configHandler").(config.ConfigHandler) - if !ok { - return fmt.Errorf("error resolving configHandler") - } - s.configHandler = configHandler - - shell, ok := s.injector.Resolve("shell").(shell.Shell) - if !ok { - return fmt.Errorf("error resolving shell") - } - s.shell = shell - - return nil -} - // WriteConfig is a no-op for the Service interface func (s *BaseService) WriteConfig() error { // No operation performed diff --git a/pkg/workstation/services/service_test.go b/pkg/workstation/services/service_test.go index 62392824b..4f9390f60 100644 --- a/pkg/workstation/services/service_test.go +++ b/pkg/workstation/services/service_test.go @@ -5,8 +5,9 @@ import ( "testing" "time" - "github.com/windsorcli/cli/pkg/runtime/config" "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/runtime" + "github.com/windsorcli/cli/pkg/runtime/config" "github.com/windsorcli/cli/pkg/runtime/shell" ) @@ -32,14 +33,13 @@ func (m *mockFileInfo) IsDir() bool { return m.isDir } func (m *mockFileInfo) Sys() interface{} { return nil } type Mocks struct { - Injector di.Injector + Runtime *runtime.Runtime ConfigHandler config.ConfigHandler Shell *shell.MockShell Shims *Shims } type SetupOptions struct { - Injector di.Injector ConfigHandler config.ConfigHandler ConfigStr string } @@ -115,33 +115,25 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { } }) - // Create injector if not provided - var injector di.Injector - if len(opts) > 0 && opts[0].Injector != nil { - injector = opts[0].Injector - } else { - injector = di.NewInjector() - } - - // Create and register mock shell first - mockShell := shell.NewMockShell(injector) + // Create mock shell + mockShell := shell.NewMockShell(nil) mockShell.GetProjectRootFunc = func() (string, error) { return tmpDir, nil } - injector.Register("shell", mockShell) // Create config handler if not provided var configHandler config.ConfigHandler if len(opts) > 0 && opts[0].ConfigHandler != nil { configHandler = opts[0].ConfigHandler } else { + // Create minimal injector for config handler initialization + injector := di.NewInjector() + injector.Register("shell", mockShell) configHandler = config.NewConfigHandler(injector) - } - injector.Register("configHandler", configHandler) - - // Initialize config handler - if err := configHandler.Initialize(); err != nil { - t.Fatalf("Failed to initialize config handler: %v", err) + // Initialize config handler + if err := configHandler.Initialize(); err != nil { + t.Fatalf("Failed to initialize config handler: %v", err) + } } configHandler.SetContext("mock-context") @@ -175,8 +167,17 @@ contexts: } } + rt := &runtime.Runtime{ + ProjectRoot: tmpDir, + ConfigRoot: tmpDir, + TemplateRoot: tmpDir, + ContextName: "mock-context", + ConfigHandler: configHandler, + Shell: mockShell, + } + return &Mocks{ - Injector: injector, + Runtime: rt, ConfigHandler: configHandler, Shell: mockShell, Shims: setupShims(t), @@ -187,57 +188,39 @@ contexts: // Test Public Methods // ============================================================================= -func TestBaseService_Initialize(t *testing.T) { +func TestBaseService_NewBaseService(t *testing.T) { setup := func(t *testing.T) (*BaseService, *Mocks) { mocks := setupMocks(t) - service := NewBaseService(mocks.Injector) + service := NewBaseService(mocks.Runtime) return service, mocks } t.Run("Success", func(t *testing.T) { // Given a set of mock components - service, _ := setup(t) - - // When a new BaseService is created and initialized - err := service.Initialize() - - // Then the initialization should succeed without errors - if err != nil { - t.Fatalf("expected no error during initialization, got %v", err) - } + service, mocks := setup(t) - // And the resolved dependencies should be set correctly + // Then the service should be created with dependencies set correctly if service.configHandler == nil { t.Fatalf("expected configHandler to be set, got nil") } if service.shell == nil { t.Fatalf("expected shell to be set, got nil") } - }) - - t.Run("ErrorResolvingShell", func(t *testing.T) { - // Given a set of mock components - service, mocks := setup(t) - - // And the injector is set to return nil for the shell dependency - mocks.Injector.Register("shell", nil) - - // When a new BaseService is created and initialized - err := service.Initialize() - - // Then the initialization should fail with an error - if err == nil { - t.Fatalf("expected an error during initialization, got nil") + if service.configHandler != mocks.ConfigHandler { + t.Fatalf("expected configHandler to match mocks") + } + if service.shell != mocks.Shell { + t.Fatalf("expected shell to match mocks") } }) + } func TestBaseService_WriteConfig(t *testing.T) { setup := func(t *testing.T) (*BaseService, *Mocks) { mocks := setupMocks(t) - service := NewBaseService(mocks.Injector) + service := NewBaseService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() return service, mocks } @@ -258,9 +241,8 @@ func TestBaseService_WriteConfig(t *testing.T) { func TestBaseService_SetAddress(t *testing.T) { setup := func(t *testing.T) (*BaseService, *Mocks) { mocks := setupMocks(t) - service := NewBaseService(mocks.Injector) + service := NewBaseService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() return service, mocks } @@ -306,9 +288,8 @@ func TestBaseService_SetAddress(t *testing.T) { func TestBaseService_GetAddress(t *testing.T) { setup := func(t *testing.T) (*BaseService, *Mocks) { mocks := setupMocks(t) - service := NewBaseService(mocks.Injector) + service := NewBaseService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() return service, mocks } @@ -331,9 +312,8 @@ func TestBaseService_GetAddress(t *testing.T) { func TestBaseService_GetName(t *testing.T) { setup := func(t *testing.T) (*BaseService, *Mocks) { mocks := setupMocks(t) - service := NewBaseService(mocks.Injector) + service := NewBaseService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() return service, mocks } @@ -356,9 +336,8 @@ func TestBaseService_GetName(t *testing.T) { func TestBaseService_GetHostname(t *testing.T) { setup := func(t *testing.T) (*BaseService, *Mocks) { mocks := setupMocks(t) - service := NewBaseService(mocks.Injector) + service := NewBaseService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() return service, mocks } @@ -397,9 +376,8 @@ func TestBaseService_GetHostname(t *testing.T) { func TestBaseService_IsLocalhostMode(t *testing.T) { setup := func(t *testing.T) (*BaseService, *Mocks) { mocks := setupMocks(t) - service := NewBaseService(mocks.Injector) + service := NewBaseService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() return service, mocks } @@ -439,9 +417,8 @@ func TestBaseService_IsLocalhostMode(t *testing.T) { func TestBaseService_SupportsWildcard(t *testing.T) { setup := func(t *testing.T) (*BaseService, *Mocks) { mocks := setupMocks(t) - service := NewBaseService(mocks.Injector) + service := NewBaseService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() return service, mocks } @@ -471,9 +448,8 @@ func TestBaseService_GetContainerName(t *testing.T) { mocks := setupMocks(t, &SetupOptions{ ConfigHandler: mockConfigHandler, }) - service := NewBaseService(mocks.Injector) + service := NewBaseService(mocks.Runtime) service.shims = mocks.Shims - service.Initialize() return service, mocks } diff --git a/pkg/workstation/services/talos_service.go b/pkg/workstation/services/talos_service.go index 13477c411..20267493f 100644 --- a/pkg/workstation/services/talos_service.go +++ b/pkg/workstation/services/talos_service.go @@ -10,7 +10,7 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/windsorcli/cli/pkg/constants" - "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/runtime" ) // The TalosService is a service component that manages Talos Linux node configuration @@ -42,9 +42,9 @@ type TalosService struct { // ============================================================================= // NewTalosService is a constructor for TalosService. -func NewTalosService(injector di.Injector, mode string) *TalosService { +func NewTalosService(rt *runtime.Runtime, mode string) *TalosService { service := &TalosService{ - BaseService: *NewBaseService(injector), + BaseService: *NewBaseService(rt), mode: mode, } diff --git a/pkg/workstation/services/talos_service_test.go b/pkg/workstation/services/talos_service_test.go index 7dae6b420..d47068cff 100644 --- a/pkg/workstation/services/talos_service_test.go +++ b/pkg/workstation/services/talos_service_test.go @@ -95,7 +95,7 @@ func TestTalosService_NewTalosService(t *testing.T) { mocks := setupTalosServiceMocks(t) // When a new TalosService is created - service := NewTalosService(mocks.Injector, "worker") + service := NewTalosService(mocks.Runtime, "worker") // Then the TalosService should not be nil if service == nil { @@ -108,7 +108,7 @@ func TestTalosService_NewTalosService(t *testing.T) { mocks := setupTalosServiceMocks(t) // When a new TalosService is created - service := NewTalosService(mocks.Injector, "controlplane") + service := NewTalosService(mocks.Runtime, "controlplane") // Then the TalosService should not be nil if service == nil { @@ -130,11 +130,8 @@ func TestTalosService_SetAddress(t *testing.T) { controlPlaneLeader = nil mocks := setupTalosServiceMocks(t) - service := NewTalosService(mocks.Injector, "controlplane") + service := NewTalosService(mocks.Runtime, "controlplane") service.SetName("controlplane1") - if err := service.Initialize(); err != nil { - t.Fatalf("Failed to initialize service: %v", err) - } return service, mocks } @@ -173,22 +170,16 @@ func TestTalosService_SetAddress(t *testing.T) { controlPlaneLeader = nil // Create a leader first - leader := NewTalosService(mocks.Injector, "controlplane") + leader := NewTalosService(mocks.Runtime, "controlplane") leader.SetName("controlplane1") - if err := leader.Initialize(); err != nil { - t.Fatalf("Failed to initialize leader service: %v", err) - } portAllocator := NewPortAllocator() if err := leader.SetAddress("192.168.1.10", portAllocator); err != nil { t.Fatalf("Failed to set leader address: %v", err) } // Create a non-leader control plane - service := NewTalosService(mocks.Injector, "controlplane") + service := NewTalosService(mocks.Runtime, "controlplane") service.SetName("controlplane2") - if err := service.Initialize(); err != nil { - t.Fatalf("Failed to initialize service: %v", err) - } // Enable localhost mode if err := mocks.ConfigHandler.Set("vm.driver", "docker-desktop"); err != nil { @@ -226,11 +217,8 @@ func TestTalosService_SetAddress(t *testing.T) { controlPlaneLeader = nil // Create a worker node - service := NewTalosService(mocks.Injector, "worker") + service := NewTalosService(mocks.Runtime, "worker") service.SetName("worker1") - if err := service.Initialize(); err != nil { - t.Fatalf("Failed to initialize service: %v", err) - } // Enable localhost mode if err := mocks.ConfigHandler.Set("vm.driver", "docker-desktop"); err != nil { @@ -269,11 +257,8 @@ func TestTalosService_SetAddress(t *testing.T) { controlPlaneLeader = nil // Create a worker node with host ports - service := NewTalosService(mocks.Injector, "worker") + service := NewTalosService(mocks.Runtime, "worker") service.SetName("worker1") - if err := service.Initialize(); err != nil { - t.Fatalf("Failed to initialize service: %v", err) - } // Configure host ports hostPorts := []string{ @@ -324,11 +309,8 @@ func TestTalosService_SetAddress(t *testing.T) { controlPlaneLeader = nil // Create a worker node - service := NewTalosService(mocks.Injector, "worker") + service := NewTalosService(mocks.Runtime, "worker") service.SetName("worker1") - if err := service.Initialize(); err != nil { - t.Fatalf("Failed to initialize service: %v", err) - } // And invalid host port format in config if err := mocks.ConfigHandler.Set("cluster.workers.hostports", []string{"invalid:format:extra"}); err != nil { @@ -355,11 +337,8 @@ func TestTalosService_SetAddress(t *testing.T) { controlPlaneLeader = nil // Create a worker node - service := NewTalosService(mocks.Injector, "worker") + service := NewTalosService(mocks.Runtime, "worker") service.SetName("worker1") - if err := service.Initialize(); err != nil { - t.Fatalf("Failed to initialize service: %v", err) - } // And invalid protocol in config if err := mocks.ConfigHandler.Set("cluster.workers.hostports", []string{"30000:30000/invalid"}); err != nil { @@ -386,11 +365,8 @@ func TestTalosService_SetAddress(t *testing.T) { controlPlaneLeader = nil // Create first worker node - service1 := NewTalosService(mocks.Injector, "worker") + service1 := NewTalosService(mocks.Runtime, "worker") service1.SetName("worker1") - if err := service1.Initialize(); err != nil { - t.Fatalf("Failed to initialize service1: %v", err) - } // Set host ports for first worker if err := mocks.ConfigHandler.Set("cluster.workers.hostports", []string{"30000:30000"}); err != nil { @@ -404,11 +380,8 @@ func TestTalosService_SetAddress(t *testing.T) { } // Create second worker node - service2 := NewTalosService(mocks.Injector, "worker") + service2 := NewTalosService(mocks.Runtime, "worker") service2.SetName("worker2") - if err := service2.Initialize(); err != nil { - t.Fatalf("Failed to initialize service2: %v", err) - } // Set same host ports for second worker if err := mocks.ConfigHandler.Set("cluster.workers.hostports", []string{"30000:30000"}); err != nil { @@ -444,11 +417,8 @@ func TestTalosService_SetAddress(t *testing.T) { } // Create a worker node - service := NewTalosService(mocks.Injector, "worker") + service := NewTalosService(mocks.Runtime, "worker") service.SetName("worker1") - if err := service.Initialize(); err != nil { - t.Fatalf("Failed to initialize service: %v", err) - } // Enable localhost mode if err := mocks.ConfigHandler.Set("vm.driver", "docker-desktop"); err != nil { @@ -487,11 +457,8 @@ func TestTalosService_SetAddress(t *testing.T) { controlPlaneLeader = nil // Create a worker node - service := NewTalosService(mocks.Injector, "worker") + service := NewTalosService(mocks.Runtime, "worker") service.SetName("worker1") - if err := service.Initialize(); err != nil { - t.Fatalf("Failed to initialize service: %v", err) - } // And invalid host port format in config hostPorts := []string{ @@ -559,11 +526,8 @@ func TestTalosService_SetAddress(t *testing.T) { controlPlaneLeader = nil // Create first worker node - service1 := NewTalosService(mocks.Injector, "worker") + service1 := NewTalosService(mocks.Runtime, "worker") service1.SetName("worker1") - if err := service1.Initialize(); err != nil { - t.Fatalf("Failed to initialize service1: %v", err) - } // Set multiple host ports for first worker hostPorts1 := []string{ @@ -582,11 +546,8 @@ func TestTalosService_SetAddress(t *testing.T) { } // Create second worker node - service2 := NewTalosService(mocks.Injector, "worker") + service2 := NewTalosService(mocks.Runtime, "worker") service2.SetName("worker2") - if err := service2.Initialize(); err != nil { - t.Fatalf("Failed to initialize service2: %v", err) - } // Set overlapping host ports for second worker hostPorts2 := []string{ @@ -622,11 +583,8 @@ func TestTalosService_SetAddress(t *testing.T) { } // Create third worker node - service3 := NewTalosService(mocks.Injector, "worker") + service3 := NewTalosService(mocks.Runtime, "worker") service3.SetName("worker3") - if err := service3.Initialize(); err != nil { - t.Fatalf("Failed to initialize service3: %v", err) - } // Set overlapping host ports for third worker hostPorts3 := []string{ @@ -681,11 +639,8 @@ func TestTalosService_SetAddress(t *testing.T) { controlPlaneLeader = nil // Create a worker node with conflicting host ports - service := NewTalosService(mocks.Injector, "worker") + service := NewTalosService(mocks.Runtime, "worker") service.SetName("worker1") - if err := service.Initialize(); err != nil { - t.Fatalf("Failed to initialize service: %v", err) - } // Configure host ports with a conflict hostPorts := []string{ @@ -735,11 +690,8 @@ func TestTalosService_SetAddress(t *testing.T) { controlPlaneLeader = nil // Create a worker node with invalid protocol - service := NewTalosService(mocks.Injector, "worker") + service := NewTalosService(mocks.Runtime, "worker") service.SetName("worker1") - if err := service.Initialize(); err != nil { - t.Fatalf("Failed to initialize service: %v", err) - } // Configure host ports with invalid protocol hostPorts := []string{ @@ -769,11 +721,8 @@ func TestTalosService_SetAddress(t *testing.T) { controlPlaneLeader = nil // Create a worker node with invalid host port format - service := NewTalosService(mocks.Injector, "worker") + service := NewTalosService(mocks.Runtime, "worker") service.SetName("worker1") - if err := service.Initialize(); err != nil { - t.Fatalf("Failed to initialize service: %v", err) - } // Configure host ports with invalid format hostPorts := []string{ @@ -803,11 +752,8 @@ func TestTalosService_SetAddress(t *testing.T) { controlPlaneLeader = nil // Create a worker node with invalid port number - service := NewTalosService(mocks.Injector, "worker") + service := NewTalosService(mocks.Runtime, "worker") service.SetName("worker1") - if err := service.Initialize(); err != nil { - t.Fatalf("Failed to initialize service: %v", err) - } // Configure host ports with invalid port number hostPorts := []string{ @@ -890,12 +836,9 @@ func TestTalosService_GetComposeConfig(t *testing.T) { controlPlaneLeader = nil mocks := setupTalosServiceMocks(t) - service := NewTalosService(mocks.Injector, "controlplane") + service := NewTalosService(mocks.Runtime, "controlplane") service.shims = mocks.Shims service.SetName("controlplane1") - if err := service.Initialize(); err != nil { - t.Fatalf("Failed to initialize service: %v", err) - } // Mock MkdirAll to always succeed service.shims.MkdirAll = func(path string, perm os.FileMode) error { @@ -912,12 +855,9 @@ func TestTalosService_GetComposeConfig(t *testing.T) { controlPlaneLeader = nil mocks := setupTalosServiceMocks(t) - service := NewTalosService(mocks.Injector, "worker") + service := NewTalosService(mocks.Runtime, "worker") service.shims = mocks.Shims service.SetName("worker1") - if err := service.Initialize(); err != nil { - t.Fatalf("Failed to initialize service: %v", err) - } // Mock MkdirAll to always succeed service.shims.MkdirAll = func(path string, perm os.FileMode) error { @@ -1121,12 +1061,9 @@ contexts: `, } mocks := setupTalosServiceMocks(t, emptyConfig) - service := NewTalosService(mocks.Injector, "controlplane") + service := NewTalosService(mocks.Runtime, "controlplane") service.shims = mocks.Shims service.SetName("controlplane1") - if err := service.Initialize(); err != nil { - t.Fatalf("Failed to initialize service: %v", err) - } // When GetComposeConfig is called config, err := service.GetComposeConfig() @@ -1159,12 +1096,9 @@ contexts: `, } mocks := setupTalosServiceMocks(t, emptyConfig) - service := NewTalosService(mocks.Injector, "controlplane") + service := NewTalosService(mocks.Runtime, "controlplane") service.shims = mocks.Shims service.SetName("controlplane1") - if err := service.Initialize(); err != nil { - t.Fatalf("Failed to initialize service: %v", err) - } // When GetComposeConfig is called config, err := service.GetComposeConfig() @@ -1286,11 +1220,8 @@ contexts: mocks.ConfigHandler.Set("dns.address", "192.168.1.1") // Create a worker node - service := NewTalosService(mocks.Injector, "worker") + service := NewTalosService(mocks.Runtime, "worker") service.SetName("worker1") - if err := service.Initialize(); err != nil { - t.Fatalf("Failed to initialize service: %v", err) - } // Mock MkdirAll to always succeed service.shims.MkdirAll = func(path string, perm os.FileMode) error { diff --git a/pkg/workstation/virt/colima_virt.go b/pkg/workstation/virt/colima_virt.go index e8d774398..1f3af40ae 100644 --- a/pkg/workstation/virt/colima_virt.go +++ b/pkg/workstation/virt/colima_virt.go @@ -16,7 +16,7 @@ import ( "time" colimaConfig "github.com/abiosoft/colima/config" - "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/runtime" ) // Test hook to force memory overflow @@ -38,10 +38,10 @@ type ColimaVirt struct { // Constructor // ============================================================================= -// NewColimaVirt creates a new instance of ColimaVirt using a DI injector -func NewColimaVirt(injector di.Injector) *ColimaVirt { +// NewColimaVirt creates a new instance of ColimaVirt +func NewColimaVirt(rt *runtime.Runtime) *ColimaVirt { return &ColimaVirt{ - BaseVirt: NewBaseVirt(injector), + BaseVirt: NewBaseVirt(rt), } } @@ -223,14 +223,17 @@ func (v *ColimaVirt) WriteConfig() error { // Maps the Go architecture to the Colima architecture format // Handles special cases for amd64 and arm64 architectures // Returns the architecture string in the format expected by Colima +// getArch returns the system architecture string formatted for Colima configuration, +// mapping standard Go architectures to their Colima equivalents using a tagged switch. func (v *ColimaVirt) getArch() string { - arch := v.BaseVirt.shims.GOARCH() - if arch == "amd64" { + switch arch := v.shims.GOARCH(); arch { + case "amd64": return "x86_64" - } else if arch == "arm64" { + case "arm64": return "aarch64" + default: + return arch } - return arch } // getDefaultValues retrieves the default values for the VM properties diff --git a/pkg/workstation/virt/colima_virt_test.go b/pkg/workstation/virt/colima_virt_test.go index f969e2a09..be070d804 100644 --- a/pkg/workstation/virt/colima_virt_test.go +++ b/pkg/workstation/virt/colima_virt_test.go @@ -16,6 +16,7 @@ import ( colimaConfig "github.com/abiosoft/colima/config" "github.com/goccy/go-yaml" "github.com/shirou/gopsutil/mem" + "github.com/windsorcli/cli/pkg/runtime" "github.com/windsorcli/cli/pkg/runtime/config" ) @@ -128,11 +129,8 @@ func TestColimaVirt_Initialize(t *testing.T) { setup := func(t *testing.T) (*ColimaVirt, *Mocks) { t.Helper() mocks := setupColimaMocks(t) - colimaVirt := NewColimaVirt(mocks.Injector) + colimaVirt := NewColimaVirt(mocks.Runtime) colimaVirt.setShims(mocks.Shims) - if err := colimaVirt.Initialize(); err != nil { - t.Fatalf("Failed to initialize ColimaVirt: %v", err) - } return colimaVirt, mocks } @@ -146,39 +144,21 @@ func TestColimaVirt_Initialize(t *testing.T) { t.Run("ErrorResolveShell", func(t *testing.T) { // Given a ColimaVirt with mock components - colimaVirt, mocks := setup(t) - - // Mock injector to return nil for shell - mocks.Injector.Register("shell", nil) - - // When calling Initialize - err := colimaVirt.Initialize() + colimaVirt, _ := setup(t) - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if !strings.Contains(err.Error(), "error resolving shell") { - t.Errorf("Expected error containing 'error resolving shell', got %v", err) + // Then the service should be properly initialized + if colimaVirt == nil { + t.Fatal("Expected ColimaVirt, got nil") } }) t.Run("ErrorResolveConfigHandler", func(t *testing.T) { // Given a ColimaVirt with mock components - colimaVirt, mocks := setup(t) - - // Mock injector to return nil for configHandler - mocks.Injector.Register("configHandler", nil) - - // When calling Initialize - err := colimaVirt.Initialize() + colimaVirt, _ := setup(t) - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if !strings.Contains(err.Error(), "error resolving configHandler") { - t.Errorf("Expected error containing 'error resolving configHandler', got %v", err) + // Then the service should be properly initialized + if colimaVirt == nil { + t.Fatal("Expected ColimaVirt, got nil") } }) } @@ -193,11 +173,8 @@ func TestColimaVirt_WriteConfig(t *testing.T) { t.Fatalf("Failed to set vm.driver: %v", err) } - colimaVirt := NewColimaVirt(mocks.Injector) + colimaVirt := NewColimaVirt(mocks.Runtime) colimaVirt.setShims(mocks.Shims) - if err := colimaVirt.Initialize(); err != nil { - t.Fatalf("Failed to initialize ColimaVirt: %v", err) - } return colimaVirt, mocks } @@ -331,11 +308,8 @@ func TestColimaVirt_WriteConfig(t *testing.T) { t.Run("NotColimaDriver", func(t *testing.T) { // Given a ColimaVirt with mock components mocks := setupColimaMocks(t) - colimaVirt := NewColimaVirt(mocks.Injector) + colimaVirt := NewColimaVirt(mocks.Runtime) colimaVirt.setShims(mocks.Shims) - if err := colimaVirt.Initialize(); err != nil { - t.Fatalf("Failed to initialize ColimaVirt: %v", err) - } // And vm.driver is not colima if err := mocks.ConfigHandler.Set("vm.driver", "other"); err != nil { @@ -495,11 +469,8 @@ func TestColimaVirt_Up(t *testing.T) { setup := func(t *testing.T) (*ColimaVirt, *Mocks) { t.Helper() mocks := setupColimaMocks(t) - colimaVirt := NewColimaVirt(mocks.Injector) + colimaVirt := NewColimaVirt(mocks.Runtime) colimaVirt.setShims(mocks.Shims) - if err := colimaVirt.Initialize(); err != nil { - t.Fatalf("Failed to initialize ColimaVirt: %v", err) - } return colimaVirt, mocks } @@ -546,7 +517,7 @@ func TestColimaVirt_Up(t *testing.T) { t.Run("ErrorSetVMAddress", func(t *testing.T) { // Given a ColimaVirt with mock components - colimaVirt, mocks := setup(t) + _, mocks := setup(t) // Create a mock config handler that returns error on set mockConfigHandler := config.NewMockConfigHandler() @@ -561,6 +532,9 @@ func TestColimaVirt_Up(t *testing.T) { mockConfigHandler.GetIntFunc = func(key string, defaultValues ...int) int { return mocks.ConfigHandler.GetInt(key, defaultValues...) } + mockConfigHandler.GetBoolFunc = func(key string, defaultValues ...bool) bool { + return mocks.ConfigHandler.GetBool(key, defaultValues...) + } // Override just the Set to return an error mockConfigHandler.SetFunc = func(key string, _ any) error { @@ -569,11 +543,29 @@ func TestColimaVirt_Up(t *testing.T) { } return nil } - mocks.Injector.Register("configHandler", mockConfigHandler) - // Re-initialize to pick up the mock config handler - if err := colimaVirt.Initialize(); err != nil { - t.Fatalf("Failed to re-initialize ColimaVirt: %v", err) + // Create a new runtime with the mock config handler + rt := &runtime.Runtime{ + ProjectRoot: mocks.Runtime.ProjectRoot, + ConfigRoot: mocks.Runtime.ConfigRoot, + TemplateRoot: mocks.Runtime.TemplateRoot, + ContextName: mocks.Runtime.ContextName, + ConfigHandler: mockConfigHandler, + Shell: mocks.Shell, + } + + // Create a new ColimaVirt with the mock config handler + colimaVirt := NewColimaVirt(rt) + + // Set up the shell to return success for colima start + mocks.Shell.ExecProgressFunc = func(message string, command string, args ...string) (string, error) { + return "", nil + } + mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { + if command == "colima" && len(args) > 0 && args[0] == "ls" { + return `{"address":"192.168.1.10","arch":"x86_64","cpus":2,"disk":60,"memory":4096,"name":"windsor-mock-context","status":"Running"}`, nil + } + return "", nil } // When calling Up @@ -593,11 +585,8 @@ func TestColimaVirt_Down(t *testing.T) { setup := func(t *testing.T) (*ColimaVirt, *Mocks) { t.Helper() mocks := setupColimaMocks(t) - colimaVirt := NewColimaVirt(mocks.Injector) + colimaVirt := NewColimaVirt(mocks.Runtime) colimaVirt.setShims(mocks.Shims) - if err := colimaVirt.Initialize(); err != nil { - t.Fatalf("Failed to initialize ColimaVirt: %v", err) - } return colimaVirt, mocks } @@ -692,11 +681,8 @@ func TestColimaVirt_getArch(t *testing.T) { setup := func(t *testing.T) (*ColimaVirt, *Mocks) { t.Helper() mocks := setupMocks(t) - colimaVirt := NewColimaVirt(mocks.Injector) + colimaVirt := NewColimaVirt(mocks.Runtime) colimaVirt.shims = mocks.Shims - if err := colimaVirt.Initialize(); err != nil { - t.Fatalf("Failed to initialize ColimaVirt: %v", err) - } return colimaVirt, mocks } @@ -748,11 +734,8 @@ func TestColimaVirt_getDefaultValues(t *testing.T) { setup := func(t *testing.T) (*ColimaVirt, *Mocks) { t.Helper() mocks := setupColimaMocks(t) - colimaVirt := NewColimaVirt(mocks.Injector) + colimaVirt := NewColimaVirt(mocks.Runtime) colimaVirt.shims = mocks.Shims - if err := colimaVirt.Initialize(); err != nil { - t.Fatalf("Failed to initialize ColimaVirt: %v", err) - } return colimaVirt, mocks } @@ -822,11 +805,8 @@ func TestColimaVirt_startColima(t *testing.T) { setup := func(t *testing.T) (*ColimaVirt, *Mocks) { t.Helper() mocks := setupColimaMocks(t) - colimaVirt := NewColimaVirt(mocks.Injector) + colimaVirt := NewColimaVirt(mocks.Runtime) colimaVirt.shims = mocks.Shims - if err := colimaVirt.Initialize(); err != nil { - t.Fatalf("Failed to initialize ColimaVirt: %v", err) - } return colimaVirt, mocks } diff --git a/pkg/workstation/virt/docker_virt.go b/pkg/workstation/virt/docker_virt.go index 46b2ae816..c59d9f037 100644 --- a/pkg/workstation/virt/docker_virt.go +++ b/pkg/workstation/virt/docker_virt.go @@ -10,13 +10,12 @@ import ( "maps" "os" "path/filepath" - "slices" "sort" "strings" "time" "github.com/compose-spec/compose-go/v2/types" - "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/runtime" "github.com/windsorcli/cli/pkg/workstation/services" ) @@ -35,52 +34,25 @@ type DockerVirt struct { // Constructor // ============================================================================= -// NewDockerVirt creates a new instance of DockerVirt using a DI injector -func NewDockerVirt(injector di.Injector) *DockerVirt { - return &DockerVirt{ - BaseVirt: *NewBaseVirt(injector), - } -} - -// ============================================================================= -// Public Methods -// ============================================================================= - -// Initialize resolves all dependencies for DockerVirt, including services from the DI -// container, Docker configuration status, and determines the appropriate docker compose -// command to use. It alphabetizes services and verifies Docker is enabled. -func (v *DockerVirt) Initialize() error { - if err := v.BaseVirt.Initialize(); err != nil { - return fmt.Errorf("error initializing base: %w", err) - } - - resolvedServices, err := v.injector.ResolveAll((*services.Service)(nil)) - if err != nil { - return fmt.Errorf("error resolving services: %w", err) - } - +// NewDockerVirt creates a new instance of DockerVirt +func NewDockerVirt(rt *runtime.Runtime, serviceList []services.Service) *DockerVirt { var serviceSlice []services.Service - for _, service := range resolvedServices { - if s, ok := service.(services.Service); ok && s != nil { - serviceSlice = append(serviceSlice, s) + if serviceList != nil { + // Filter out nil services and copy non-nil ones + for _, service := range serviceList { + if service != nil { + serviceSlice = append(serviceSlice, service) + } } + sort.Slice(serviceSlice, func(i, j int) bool { + return serviceSlice[i].GetName() < serviceSlice[j].GetName() + }) } - sort.Slice(serviceSlice, func(i, j int) bool { - return serviceSlice[i].GetName() < serviceSlice[j].GetName() - }) - - if !v.configHandler.GetBool("docker.enabled") { - return fmt.Errorf("Docker configuration is not defined") - } - - v.services = serviceSlice - - if err := v.determineComposeCommand(); err != nil { - return fmt.Errorf("error determining docker compose command: %w", err) + return &DockerVirt{ + BaseVirt: *NewBaseVirt(rt), + services: serviceSlice, } - - return nil } // Up starts Docker Compose in detached mode with retry logic. It checks if Docker is enabled, @@ -94,16 +66,17 @@ func (v *DockerVirt) Up() error { return fmt.Errorf("Docker daemon is not running: %w", err) } + if err := v.determineComposeCommand(); err != nil { + return fmt.Errorf("failed to determine compose command: %w", err) + } + if v.configHandler.GetString("vm.driver") == "colima" { if err := v.WriteConfig(); err != nil { return fmt.Errorf("error regenerating docker compose config: %w", err) } } - projectRoot, err := v.shell.GetProjectRoot() - if err != nil { - return fmt.Errorf("error retrieving project root: %w", err) - } + projectRoot := v.runtime.ProjectRoot composeFilePath := filepath.Join(projectRoot, ".windsor", "docker-compose.yaml") if err := v.shims.Setenv("COMPOSE_FILE", composeFilePath); err != nil { @@ -118,14 +91,14 @@ func (v *DockerVirt) Up() error { message := "📦 Running docker compose up" if i == 0 { - output, err := v.shell.ExecProgress(message, v.composeCommand, args...) + output, err := v.execComposeCommand(message, args...) if err == nil { return nil } lastErr = err lastOutput = output } else { - output, err := v.shell.ExecSilent(v.composeCommand, args...) + output, err := v.execComposeCommandSilent(args...) if err == nil { return nil } @@ -154,10 +127,11 @@ func (v *DockerVirt) Down() error { return fmt.Errorf("Docker daemon is not running: %w", err) } - projectRoot, err := v.shell.GetProjectRoot() - if err != nil { - return fmt.Errorf("error retrieving project root: %w", err) + if err := v.determineComposeCommand(); err != nil { + return fmt.Errorf("failed to determine compose command: %w", err) } + + projectRoot := v.runtime.ProjectRoot composeFilePath := filepath.Join(projectRoot, ".windsor", "docker-compose.yaml") if _, err := v.shims.Stat(composeFilePath); os.IsNotExist(err) { @@ -169,7 +143,7 @@ func (v *DockerVirt) Down() error { return fmt.Errorf("error setting COMPOSE_FILE environment variable: %w", err) } - output, err := v.shell.ExecProgress("📦 Running docker compose down", v.composeCommand, "down", "--remove-orphans", "--volumes") + output, err := v.execComposeCommand("📦 Running docker compose down", "down", "--remove-orphans", "--volumes") if err != nil { return fmt.Errorf("Error executing command %s down: %w\n%s", v.composeCommand, err, output) } @@ -182,10 +156,7 @@ func (v *DockerVirt) Down() error { // the full compose configuration, serializes it to YAML, and writes it to the .windsor // directory with appropriate permissions. func (v *DockerVirt) WriteConfig() error { - projectRoot, err := v.shell.GetProjectRoot() - if err != nil { - return fmt.Errorf("error retrieving project root: %w", err) - } + projectRoot := v.runtime.ProjectRoot composeFilePath := filepath.Join(projectRoot, ".windsor", "docker-compose.yaml") if err := v.shims.MkdirAll(filepath.Dir(composeFilePath), 0755); err != nil { @@ -210,77 +181,6 @@ func (v *DockerVirt) WriteConfig() error { return nil } -// getContainerInfo retrieves detailed information about Docker containers managed by -// Windsor, including their names, IP addresses, and labels. It filters containers -// by Windsor-managed labels and context, and optionally by service name if provided. -// For each container, it retrieves network settings to determine IP addresses. -func (v *DockerVirt) getContainerInfo(name ...string) ([]ContainerInfo, error) { - contextName := v.configHandler.GetContext() - - command := "docker" - args := []string{"ps", "--filter", "label=managed_by=windsor", "--filter", fmt.Sprintf("label=context=%s", contextName), "--format", "{{.ID}}"} - out, err := v.shell.ExecSilent(command, args...) - if err != nil { - return nil, err - } - - containerIDs := strings.Split(strings.TrimSpace(out), "\n") - var containerInfos []ContainerInfo - - for _, containerID := range containerIDs { - if containerID == "" { - continue - } - inspectArgs := []string{"inspect", containerID, "--format", "{{json .Config.Labels}}"} - inspectOut, err := v.shell.ExecSilent(command, inspectArgs...) - if err != nil { - return nil, err - } - - var labels map[string]string - if err := v.shims.UnmarshalJSON([]byte(inspectOut), &labels); err != nil { - return nil, fmt.Errorf("error unmarshaling container labels: %w", err) - } - - serviceName, _ := labels["com.docker.compose.service"] - - networkInspectArgs := []string{"inspect", containerID, "--format", "{{json .NetworkSettings.Networks}}"} - networkInspectOut, err := v.shell.ExecSilent(command, networkInspectArgs...) - if err != nil { - return nil, fmt.Errorf("error inspecting container networks: %w", err) - } - - var networks map[string]struct { - IPAddress string `json:"IPAddress"` - } - if err := v.shims.UnmarshalJSON([]byte(networkInspectOut), &networks); err != nil { - return nil, fmt.Errorf("error unmarshaling container networks: %w", err) - } - - var ipAddress string - networkKey := fmt.Sprintf("windsor-%s", contextName) - if network, exists := networks[networkKey]; exists { - ipAddress = network.IPAddress - } - - containerInfo := ContainerInfo{ - Name: serviceName, - Address: ipAddress, - Labels: labels, - } - - if len(name) > 0 { - if slices.Contains(name, serviceName) { - containerInfos = append(containerInfos, containerInfo) - } - } else { - containerInfos = append(containerInfos, containerInfo) - } - } - - return containerInfos, nil -} - // Ensure DockerVirt implements ContainerRuntime var _ ContainerRuntime = (*DockerVirt)(nil) @@ -294,7 +194,13 @@ var _ ContainerRuntime = (*DockerVirt)(nil) func (v *DockerVirt) determineComposeCommand() error { commands := []string{"docker-compose", "docker-cli-plugin-docker-compose", "docker compose"} for _, cmd := range commands { - if _, err := v.shell.ExecSilent(cmd, "--version"); err == nil { + cmdParts := strings.Fields(cmd) + if len(cmdParts) == 0 { + continue + } + command := cmdParts[0] + args := append(cmdParts[1:], "--version") + if _, err := v.shell.ExecSilent(command, args...); err == nil { v.composeCommand = cmd return nil } @@ -302,6 +208,30 @@ func (v *DockerVirt) determineComposeCommand() error { return nil } +// execComposeCommand executes the compose command with progress indicator, handling +// commands that may contain spaces (e.g., "docker compose"). +func (v *DockerVirt) execComposeCommand(message string, args ...string) (string, error) { + cmdParts := strings.Fields(v.composeCommand) + if len(cmdParts) == 0 { + return "", fmt.Errorf("compose command is empty") + } + command := cmdParts[0] + allArgs := append(cmdParts[1:], args...) + return v.shell.ExecProgress(message, command, allArgs...) +} + +// execComposeCommandSilent executes the compose command silently, handling +// commands that may contain spaces (e.g., "docker compose"). +func (v *DockerVirt) execComposeCommandSilent(args ...string) (string, error) { + cmdParts := strings.Fields(v.composeCommand) + if len(cmdParts) == 0 { + return "", fmt.Errorf("compose command is empty") + } + command := cmdParts[0] + allArgs := append(cmdParts[1:], args...) + return v.shell.ExecSilent(command, allArgs...) +} + // checkDockerDaemon verifies that the Docker daemon is running and accessible by // executing the 'docker info' command. It returns an error if the daemon cannot // be contacted, which is used by other functions to ensure Docker is available. diff --git a/pkg/workstation/virt/docker_virt_test.go b/pkg/workstation/virt/docker_virt_test.go index 947d99549..9cbdbdac0 100644 --- a/pkg/workstation/virt/docker_virt_test.go +++ b/pkg/workstation/virt/docker_virt_test.go @@ -166,71 +166,39 @@ func TestDockerVirt_Initialize(t *testing.T) { setup := func(t *testing.T) (*DockerVirt, *Mocks) { t.Helper() mocks := setupDockerMocks(t) - dockerVirt := NewDockerVirt(mocks.Injector) + dockerVirt := NewDockerVirt(mocks.Runtime, []services.Service{}) dockerVirt.shims = mocks.Shims - // Register default mock service - mocks.Injector.Register("defaultService", mocks.Service) return dockerVirt, mocks } t.Run("Success", func(t *testing.T) { // Given a docker virt instance with valid mocks - dockerVirt, _ := setup(t) - - // When initializing - err := dockerVirt.Initialize() - - // Then no error should occur - if err != nil { - t.Errorf("expected no error, got %v", err) - } + mocks := setupDockerMocks(t) + serviceList := []services.Service{mocks.Service} + dockerVirt := NewDockerVirt(mocks.Runtime, serviceList) + dockerVirt.shims = mocks.Shims // And services should be resolved if len(dockerVirt.services) == 0 { t.Errorf("expected services to be resolved, but got none") } + if len(dockerVirt.services) != 1 { + t.Errorf("expected 1 service, got %d", len(dockerVirt.services)) + } }) t.Run("ErrorInitializingBaseVirt", func(t *testing.T) { - // Given a docker virt instance with invalid shell - dockerVirt, mocks := setup(t) - mocks.Injector.Register("shell", "not a shell") - - // When initializing - err := dockerVirt.Initialize() - - // Then an error should occur - if err == nil { - t.Errorf("expected error, got none") - } + // Given a docker virt instance with mock components + dockerVirt, _ := setup(t) - // And the error should contain the expected message - expectedErrorSubstring := "error resolving shell" - if !strings.Contains(err.Error(), expectedErrorSubstring) { - t.Errorf("expected error message to contain %q, got %q", expectedErrorSubstring, err.Error()) + // Then the service should be properly initialized + if dockerVirt == nil { + t.Fatal("Expected DockerVirt, got nil") } }) - t.Run("ErrorDockerNotEnabled", func(t *testing.T) { - // Given a docker virt instance with docker disabled - dockerVirt, mocks := setup(t) - if err := mocks.ConfigHandler.Set("docker.enabled", false); err != nil { - t.Fatalf("Failed to set docker.enabled: %v", err) - } - - // When initializing - err := dockerVirt.Initialize() - - // Then an error should occur - if err == nil { - t.Errorf("expected error, got none") - } - if !strings.Contains(err.Error(), "Docker configuration is not defined") { - t.Errorf("expected error about Docker not being enabled, got %v", err) - } - }) t.Run("ErrorResolvingServices", func(t *testing.T) { // Given a docker virt instance with failing service resolution @@ -243,18 +211,12 @@ func TestDockerVirt_Initialize(t *testing.T) { mockInjector.SetResolveAllError((*services.Service)(nil), fmt.Errorf("service resolution failed")) // Replace injector and recreate dockerVirt - dockerVirt = NewDockerVirt(mockInjector) + dockerVirt = NewDockerVirt(mocks.Runtime, []services.Service{}) dockerVirt.shims = mocks.Shims - // When initializing - err := dockerVirt.Initialize() - - // Then an error should occur - if err == nil { - t.Errorf("expected error, got none") - } - if !strings.Contains(err.Error(), "error resolving services") { - t.Errorf("expected error about resolving services, got %v", err) + // Then the service should be properly initialized + if dockerVirt == nil { + t.Fatal("Expected DockerVirt, got nil") } }) @@ -274,13 +236,6 @@ func TestDockerVirt_Initialize(t *testing.T) { return "", fmt.Errorf("unexpected command: %s %v", command, args) } - // When initializing - err := dockerVirt.Initialize() - - // Then no error should occur - if err != nil { - t.Errorf("expected no error, got %v", err) - } // And the compose command should be empty if dockerVirt.composeCommand != "" { @@ -290,26 +245,24 @@ func TestDockerVirt_Initialize(t *testing.T) { t.Run("NilServiceInSlice", func(t *testing.T) { // Given a docker virt instance with a nil service - dockerVirt, mocks := setup(t) - mocks.Injector.Register("nilService", nil) - - // When initializing - err := dockerVirt.Initialize() + mocks := setupDockerMocks(t) + serviceList := []services.Service{mocks.Service, nil} + dockerVirt := NewDockerVirt(mocks.Runtime, serviceList) + dockerVirt.shims = mocks.Shims - // Then no error should occur - if err != nil { - t.Errorf("Expected no error, got: %v", err) + // Then services slice should only contain the non-nil service (nil is filtered out) + if len(dockerVirt.services) != 1 { + t.Errorf("Expected 1 service (nil is filtered out), got %d", len(dockerVirt.services)) } - - // And services slice should only contain the default service - if len(dockerVirt.services) != 2 { - t.Errorf("Expected 2 services (default + nil), got %d", len(dockerVirt.services)) + // Verify the service is not nil + if dockerVirt.services[0] == nil { + t.Error("Expected service to not be nil") } }) t.Run("ServicesAreSorted", func(t *testing.T) { // Given a docker virt instance with multiple services - dockerVirt, mocks := setup(t) + mocks := setupDockerMocks(t) // And services in random order serviceA := services.NewMockService() @@ -318,29 +271,22 @@ func TestDockerVirt_Initialize(t *testing.T) { serviceA.SetName("ServiceA") serviceB.SetName("ServiceB") serviceC.SetName("ServiceC") - mocks.Injector.Register("serviceA", serviceA) - mocks.Injector.Register("serviceB", serviceB) - mocks.Injector.Register("serviceC", serviceC) - - // When initializing - err := dockerVirt.Initialize() - // Then no error should occur - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } + // Create service list in random order + serviceList := []services.Service{serviceC, serviceA, serviceB, mocks.Service} + dockerVirt := NewDockerVirt(mocks.Runtime, serviceList) + dockerVirt.shims = mocks.Shims - // And services should be sorted by name - if len(dockerVirt.services) != 5 { - t.Errorf("Expected 5 services (default + 3 registered + 1 from config), got %d", len(dockerVirt.services)) + // Then services should be sorted by name + if len(dockerVirt.services) != 4 { + t.Errorf("Expected 4 services, got %d", len(dockerVirt.services)) } - if len(dockerVirt.services) == 5 { + if len(dockerVirt.services) == 4 { serviceNames := []string{ dockerVirt.services[0].GetName(), dockerVirt.services[1].GetName(), dockerVirt.services[2].GetName(), dockerVirt.services[3].GetName(), - dockerVirt.services[4].GetName(), } if !sort.StringsAreSorted(serviceNames) { t.Errorf("Services are not sorted by name: %v", serviceNames) @@ -359,15 +305,12 @@ func TestDockerVirt_Initialize(t *testing.T) { mockInjector.Register("nilService", nil) // Replace injector and recreate dockerVirt - dockerVirt = NewDockerVirt(mockInjector) + dockerVirt = NewDockerVirt(mocks.Runtime, []services.Service{}) dockerVirt.shims = mocks.Shims - // When initializing - err := dockerVirt.Initialize() - - // Then no error should occur - if err != nil { - t.Errorf("expected no error, got %v", err) + // Then the service should be properly initialized + if dockerVirt == nil { + t.Fatal("Expected DockerVirt, got nil") } }) } @@ -377,11 +320,8 @@ func TestDockerVirt_Up(t *testing.T) { setup := func(t *testing.T) (*DockerVirt, *Mocks) { t.Helper() mocks := setupDockerMocks(t) - dockerVirt := NewDockerVirt(mocks.Injector) + dockerVirt := NewDockerVirt(mocks.Runtime, []services.Service{}) dockerVirt.shims = mocks.Shims - if err := dockerVirt.Initialize(); err != nil { - t.Fatalf("Failed to initialize DockerVirt: %v", err) - } return dockerVirt, mocks } @@ -402,9 +342,22 @@ func TestDockerVirt_Up(t *testing.T) { // Given a DockerVirt with mock components dockerVirt, mocks := setup(t) + // Mock ExecSilent to handle determineComposeCommand + oldExecSilent := mocks.Shell.ExecSilentFunc + mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { + if len(args) > 0 && args[0] == "--version" { + // Handle determineComposeCommand check + if command == "docker-compose" { + return "docker-compose version 1.29.2", nil + } + } + return oldExecSilent(command, args...) + } + // Mock command execution to fail + cmdParts := []string{"docker-compose"} mocks.Shell.ExecProgressFunc = func(message string, command string, args ...string) (string, error) { - if command == dockerVirt.composeCommand && args[0] == "up" { + if command == cmdParts[0] && len(args) > 0 && args[0] == "up" { return "", fmt.Errorf("mock docker-compose up error") } return "", fmt.Errorf("unexpected command: %s %v", command, args) @@ -426,27 +379,8 @@ func TestDockerVirt_Up(t *testing.T) { }) t.Run("ErrorGetConfigRoot", func(t *testing.T) { - // Given a DockerVirt with mock components - dockerVirt, mocks := setup(t) - - // Override GetProjectRoot to return an error - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("mock error retrieving project root") - } - - // When calling the Up method - err := dockerVirt.Up() - - // Then an error should occur - if err == nil { - t.Errorf("expected an error, got nil") - } - - // And the error should contain the expected message - expectedErrorMsg := "error retrieving project root" - if err != nil && !strings.Contains(err.Error(), expectedErrorMsg) { - t.Errorf("expected error message to contain %q, got %v", expectedErrorMsg, err) - } + // This test is obsolete - ProjectRoot is now a direct field on runtime, not retrieved via a function + t.Skip("ProjectRoot is now a direct field, cannot test retrieval error") }) t.Run("ErrorSettingComposeFileEnv", func(t *testing.T) { @@ -473,11 +407,8 @@ func TestDockerVirt_Up(t *testing.T) { } // Create and initialize DockerVirt - dockerVirt := NewDockerVirt(mocks.Injector) + dockerVirt := NewDockerVirt(mocks.Runtime, []services.Service{}) dockerVirt.shims = mocks.Shims - if err := dockerVirt.Initialize(); err != nil { - t.Fatalf("Failed to initialize DockerVirt: %v", err) - } // When calling the Up method err := dockerVirt.Up() @@ -523,35 +454,49 @@ func TestDockerVirt_Up(t *testing.T) { // Track command execution count execCount := 0 - // Mock command execution to fail twice then succeed - mocks.Shell.ExecProgressFunc = func(message string, command string, args ...string) (string, error) { - if command == dockerVirt.composeCommand && args[0] == "up" { + // Mock ExecSilent to handle determineComposeCommand and ensure we get "docker compose" + oldExecSilent := mocks.Shell.ExecSilentFunc + mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { + // Handle determineComposeCommand version check + // For "docker compose", it splits to ["docker", "compose"] and calls ExecSilent("docker", "compose", "--version") + if len(args) > 0 && args[len(args)-1] == "--version" { + if command == "docker-compose" { + return "", fmt.Errorf("docker-compose not found") + } + if command == "docker-cli-plugin-docker-compose" { + return "", fmt.Errorf("docker-cli-plugin-docker-compose not found") + } + if command == "docker" && len(args) > 0 && args[0] == "compose" { + return "Docker Compose version 2.0.0", nil + } + } + // Keep original behavior for other commands + if command == "docker" && len(args) > 0 && args[0] == "info" { + return oldExecSilent(command, args...) + } + + // Handle compose up retry attempts (docker compose up) + if command == "docker" && len(args) > 0 && args[0] == "compose" && len(args) > 1 && args[1] == "up" { execCount++ if execCount < 3 { return "", fmt.Errorf("temporary error") } return "success", nil } + return "", fmt.Errorf("unexpected command: %s %v", command, args) } - // And ExecSilent for retries also fails twice then succeeds - oldExecSilent := mocks.Shell.ExecSilentFunc - mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { - // Keep original behavior for commands used in setup - if command == "docker" && (len(args) > 0 && args[0] == "compose" || len(args) > 0 && args[0] == "info") { - return oldExecSilent(command, args...) - } - - // Handle compose up retry attempts - if command == dockerVirt.composeCommand && len(args) > 0 && args[0] == "up" { + // Mock ExecProgress to fail twice then succeed + mocks.Shell.ExecProgressFunc = func(message string, command string, args ...string) (string, error) { + // Handle compose up (docker compose up) + if command == "docker" && len(args) > 0 && args[0] == "compose" && len(args) > 1 && args[1] == "up" { execCount++ if execCount < 3 { return "", fmt.Errorf("temporary error") } return "success", nil } - return "", fmt.Errorf("unexpected command: %s %v", command, args) } @@ -575,11 +520,8 @@ func TestDockerVirt_Down(t *testing.T) { setup := func(t *testing.T) (*DockerVirt, *Mocks) { t.Helper() mocks := setupDockerMocks(t) - dockerVirt := NewDockerVirt(mocks.Injector) + dockerVirt := NewDockerVirt(mocks.Runtime, []services.Service{}) dockerVirt.shims = mocks.Shims - if err := dockerVirt.Initialize(); err != nil { - t.Fatalf("Failed to initialize DockerVirt: %v", err) - } return dockerVirt, mocks } @@ -647,27 +589,8 @@ func TestDockerVirt_Down(t *testing.T) { }) t.Run("ErrorGetConfigRoot", func(t *testing.T) { - // Given a DockerVirt with mock components - dockerVirt, mocks := setup(t) - - // Override GetProjectRoot to return an error - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("mock error retrieving project root") - } - - // When calling the Down method - err := dockerVirt.Down() - - // Then an error should occur - if err == nil { - t.Errorf("expected an error, got nil") - } - - // And the error should contain the expected message - expectedErrorMsg := "error retrieving project root" - if err != nil && !strings.Contains(err.Error(), expectedErrorMsg) { - t.Errorf("expected error message to contain %q, got %v", expectedErrorMsg, err) - } + // This test is obsolete - ProjectRoot is now a direct field on runtime, not retrieved via a function + t.Skip("ProjectRoot is now a direct field, cannot test retrieval error") }) t.Run("ErrorSettingComposeFileEnv", func(t *testing.T) { @@ -683,9 +606,6 @@ func TestDockerVirt_Down(t *testing.T) { } // Re-initialize DockerVirt to establish the necessary compose commands - if err := dockerVirt.Initialize(); err != nil { - t.Fatalf("Failed to initialize DockerVirt: %v", err) - } // When calling the Down method err := dockerVirt.Down() @@ -727,8 +647,15 @@ func TestDockerVirt_Down(t *testing.T) { t.Run("ErrorExecutingComposeDown", func(t *testing.T) { // Given a docker virt instance with failing compose down dockerVirt, mocks := setup(t) + + // Determine compose command first + if err := dockerVirt.determineComposeCommand(); err != nil { + t.Fatalf("Failed to determine compose command: %v", err) + } + + cmdParts := strings.Fields(dockerVirt.composeCommand) mocks.Shell.ExecProgressFunc = func(message string, command string, args ...string) (string, error) { - if command == dockerVirt.composeCommand && args[0] == "down" { + if command == cmdParts[0] && len(args) > 0 && args[0] == cmdParts[1] && len(args) > 1 && args[1] == "down" { return "mock error output", fmt.Errorf("mock error executing down command") } return "", fmt.Errorf("unexpected command: %s %v", command, args) @@ -755,17 +682,17 @@ func TestDockerVirt_WriteConfig(t *testing.T) { setup := func(t *testing.T) (*DockerVirt, *Mocks) { t.Helper() mocks := setupDockerMocks(t) - dockerVirt := NewDockerVirt(mocks.Injector) + dockerVirt := NewDockerVirt(mocks.Runtime, []services.Service{}) dockerVirt.shims = mocks.Shims - if err := dockerVirt.Initialize(); err != nil { - t.Fatalf("Failed to initialize DockerVirt: %v", err) - } return dockerVirt, mocks } t.Run("Success", func(t *testing.T) { // Given a virt instance with mock components - dockerVirt, mocks := setup(t) + mocks := setupDockerMocks(t) + serviceList := []services.Service{mocks.Service} + dockerVirt := NewDockerVirt(mocks.Runtime, serviceList) + dockerVirt.shims = mocks.Shims // Track written content var writtenContent []byte @@ -789,22 +716,8 @@ func TestDockerVirt_WriteConfig(t *testing.T) { }) t.Run("ErrorGetProjectRoot", func(t *testing.T) { - // Given a docker virt instance with failing shell - dockerVirt, mocks := setup(t) - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("failed to get project root") - } - - // When writing config - err := dockerVirt.WriteConfig() - - // Then an error should occur - if err == nil { - t.Errorf("expected error, got none") - } - if !strings.Contains(err.Error(), "error retrieving project root") { - t.Errorf("expected error about project root, got %v", err) - } + // This test is obsolete - ProjectRoot is now a direct field on runtime, not retrieved via a function + t.Skip("ProjectRoot is now a direct field, cannot test retrieval error") }) t.Run("ErrorMkdirAll", func(t *testing.T) { @@ -870,7 +783,7 @@ func TestDockerVirt_DetermineComposeCommand(t *testing.T) { setup := func(t *testing.T) (*DockerVirt, *Mocks) { t.Helper() mocks := setupDockerMocks(t) - dockerVirt := NewDockerVirt(mocks.Injector) + dockerVirt := NewDockerVirt(mocks.Runtime, []services.Service{}) dockerVirt.shims = mocks.Shims return dockerVirt, mocks } @@ -887,12 +800,9 @@ func TestDockerVirt_DetermineComposeCommand(t *testing.T) { return "", fmt.Errorf("unexpected command: %s %v", command, args) } - // When initializing - err := dockerVirt.Initialize() - - // Then no error should occur - if err != nil { - t.Errorf("expected no error, got %v", err) + // When determining compose command + if err := dockerVirt.determineComposeCommand(); err != nil { + t.Fatalf("Expected no error, got %v", err) } // And the compose command should be set to docker-compose @@ -916,12 +826,9 @@ func TestDockerVirt_DetermineComposeCommand(t *testing.T) { return "", fmt.Errorf("unexpected command: %s %v", command, args) } - // When initializing - err := dockerVirt.Initialize() - - // Then no error should occur - if err != nil { - t.Errorf("expected no error, got %v", err) + // When determining compose command + if err := dockerVirt.determineComposeCommand(); err != nil { + t.Fatalf("Expected no error, got %v", err) } // And the compose command should be set to docker-cli-plugin-docker-compose @@ -942,18 +849,15 @@ func TestDockerVirt_DetermineComposeCommand(t *testing.T) { if command == "docker-cli-plugin-docker-compose" { return "", fmt.Errorf("docker-cli-plugin-docker-compose not found") } - if command == "docker compose" { + if command == "docker" && len(args) > 0 && args[0] == "compose" { return "Docker Compose version 2.0.0", nil } return "", fmt.Errorf("unexpected command: %s %v", command, args) } - // When initializing - err := dockerVirt.Initialize() - - // Then no error should occur - if err != nil { - t.Errorf("expected no error, got %v", err) + // When determining compose command + if err := dockerVirt.determineComposeCommand(); err != nil { + t.Fatalf("Expected no error, got %v", err) } // And the compose command should be set to docker compose @@ -980,13 +884,6 @@ func TestDockerVirt_DetermineComposeCommand(t *testing.T) { return "", fmt.Errorf("unexpected command: %s %v", command, args) } - // When initializing - err := dockerVirt.Initialize() - - // Then no error should occur - if err != nil { - t.Errorf("expected no error, got %v", err) - } // And the compose command should be empty if dockerVirt.composeCommand != "" { @@ -1000,17 +897,17 @@ func TestDockerVirt_GetFullComposeConfig(t *testing.T) { setup := func(t *testing.T) (*DockerVirt, *Mocks) { t.Helper() mocks := setupDockerMocks(t) - dockerVirt := NewDockerVirt(mocks.Injector) + dockerVirt := NewDockerVirt(mocks.Runtime, []services.Service{}) dockerVirt.shims = mocks.Shims - if err := dockerVirt.Initialize(); err != nil { - t.Fatalf("Failed to initialize DockerVirt: %v", err) - } return dockerVirt, mocks } t.Run("Success", func(t *testing.T) { // Given a docker virt instance with valid mocks - dockerVirt, _ := setup(t) + mocks := setupDockerMocks(t) + serviceList := []services.Service{mocks.Service} + dockerVirt := NewDockerVirt(mocks.Runtime, serviceList) + dockerVirt.shims = mocks.Shims // When getting the full compose config project, err := dockerVirt.getFullComposeConfig() @@ -1026,6 +923,7 @@ func TestDockerVirt_GetFullComposeConfig(t *testing.T) { } // And the project should have the expected services + // The mock service returns a compose config with 2 services (service1 and service2) if len(project.Services) != 2 { t.Errorf("expected 2 services, got %d", len(project.Services)) } @@ -1330,3 +1228,4 @@ func TestDockerVirt_GetFullComposeConfig(t *testing.T) { } }) } + diff --git a/pkg/workstation/virt/virt.go b/pkg/workstation/virt/virt.go index ae2c98303..a72593315 100644 --- a/pkg/workstation/virt/virt.go +++ b/pkg/workstation/virt/virt.go @@ -6,11 +6,9 @@ package virt import ( - "fmt" - "os" - "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/runtime" "github.com/windsorcli/cli/pkg/runtime/config" "github.com/windsorcli/cli/pkg/runtime/shell" ) @@ -46,7 +44,7 @@ type ContainerInfo struct { } type BaseVirt struct { - injector di.Injector + runtime *runtime.Runtime shell shell.Shell configHandler config.ConfigHandler shims *Shims @@ -58,7 +56,6 @@ type BaseVirt struct { // Virt defines methods for the virt operations type Virt interface { - Initialize() error Up() error Down() error WriteConfig() error @@ -79,32 +76,13 @@ type ContainerRuntime interface { // ============================================================================= // NewBaseVirt creates a new BaseVirt instance -func NewBaseVirt(injector di.Injector) *BaseVirt { +func NewBaseVirt(rt *runtime.Runtime) *BaseVirt { return &BaseVirt{ - injector: injector, - shims: NewShims(), - } -} - -// ============================================================================= -// Public Methods -// ============================================================================= - -// Initialize is a method that initializes the virt environment -func (v *BaseVirt) Initialize() error { - shellInstance, ok := v.injector.Resolve("shell").(shell.Shell) - if !ok { - return fmt.Errorf("error resolving shell") + runtime: rt, + shell: rt.Shell, + configHandler: rt.ConfigHandler, + shims: NewShims(), } - v.shell = shellInstance - - configHandler, ok := v.injector.Resolve("configHandler").(config.ConfigHandler) - if !ok { - return fmt.Errorf("error resolving configHandler") - } - v.configHandler = configHandler - - return nil } // setShims sets the shims for testing purposes diff --git a/pkg/workstation/virt/virt_test.go b/pkg/workstation/virt/virt_test.go index 8ae735673..43ea4fa39 100644 --- a/pkg/workstation/virt/virt_test.go +++ b/pkg/workstation/virt/virt_test.go @@ -9,13 +9,13 @@ import ( "encoding/json" "io" "os" - "strings" "testing" "github.com/goccy/go-yaml" "github.com/shirou/gopsutil/mem" - "github.com/windsorcli/cli/pkg/runtime/config" "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/runtime" + "github.com/windsorcli/cli/pkg/runtime/config" "github.com/windsorcli/cli/pkg/workstation/services" "github.com/windsorcli/cli/pkg/runtime/shell" ) @@ -25,7 +25,7 @@ import ( // ============================================================================= type Mocks struct { - Injector di.Injector + Runtime *runtime.Runtime ConfigHandler config.ConfigHandler Shell *shell.MockShell Shims *Shims @@ -33,7 +33,6 @@ type Mocks struct { } type SetupOptions struct { - Injector di.Injector ConfigHandler config.ConfigHandler ConfigStr string } @@ -120,39 +119,40 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { options = opts[0] } - // Create injector - var injector di.Injector - if options.Injector == nil { - injector = di.NewInjector() - } else { - injector = options.Injector - } - // Create shell mockShell := shell.NewMockShell() // Mock GetProjectRoot to return a temporary directory mockShell.GetProjectRootFunc = func() (string, error) { return t.TempDir(), nil } - injector.Register("shell", mockShell) // Create config handler var configHandler config.ConfigHandler if options.ConfigHandler == nil { + // Create minimal injector for config handler initialization + injector := di.NewInjector() + injector.Register("shell", mockShell) configHandler = config.NewConfigHandler(injector) + // Initialize config handler + if err := configHandler.Initialize(); err != nil { + t.Fatalf("Failed to initialize config handler: %v", err) + } } else { configHandler = options.ConfigHandler } // Create mock service mockService := services.NewMockService() - injector.Register("service", mockService) - - // Register dependencies - injector.Register("configHandler", configHandler) - // Initialize config handler - configHandler.Initialize() + // Create runtime + rt := &runtime.Runtime{ + ProjectRoot: tmpDir, + ConfigRoot: tmpDir, + TemplateRoot: tmpDir, + ContextName: "mock-context", + ConfigHandler: configHandler, + Shell: mockShell, + } configHandler.SetContext("mock-context") // Load default config string @@ -189,7 +189,7 @@ contexts: }) return &Mocks{ - Injector: injector, + Runtime: rt, ConfigHandler: configHandler, Shell: mockShell, Service: mockService, @@ -205,59 +205,38 @@ func TestVirt_Initialize(t *testing.T) { setup := func(t *testing.T) (*Mocks, *BaseVirt) { t.Helper() mocks := setupMocks(t) - virt := NewBaseVirt(mocks.Injector) + virt := NewBaseVirt(mocks.Runtime) virt.shims = mocks.Shims return mocks, virt } t.Run("Success", func(t *testing.T) { - // Given a Virt with a mock injector + // Given a Virt with a mock runtime _, virt := setup(t) - // When calling Initialize - err := virt.Initialize() - - // Then no error should be returned - if err != nil { - t.Fatalf("Expected no error, got %v", err) + // Then the service should be properly initialized + if virt == nil { + t.Fatal("Expected Virt, got nil") } }) t.Run("ErrorResolvingShell", func(t *testing.T) { - // Given a Virt with an invalid shell - injector := di.NewMockInjector() - mockConfigHandler := config.NewMockConfigHandler() - - injector.Register("configHandler", mockConfigHandler) - injector.Register("shell", "invalid") - virt := NewBaseVirt(injector) - virt.shims = NewShims() - - // When calling Initialize - err := virt.Initialize() + // Given a Virt with mock components + _, virt := setup(t) - // Then an error should be returned - if err == nil || !strings.Contains(err.Error(), "error resolving shell") { - t.Fatalf("Expected error containing 'error resolving shell', got %v", err) + // Then the service should be properly initialized + if virt == nil { + t.Fatal("Expected Virt, got nil") } }) t.Run("ErrorResolvingConfigHandler", func(t *testing.T) { - // Given a Virt with an invalid config handler - injector := di.NewMockInjector() - mockShell := shell.NewMockShell() - - injector.Register("shell", mockShell) - injector.Register("configHandler", "invalid") - virt := NewBaseVirt(injector) - virt.shims = NewShims() - - // When calling Initialize - err := virt.Initialize() + // Given a Virt with mock components + _, virt := setup(t) - // Then an error should be returned - if err == nil || !strings.Contains(err.Error(), "error resolving configHandler") { - t.Fatalf("Expected error containing 'error resolving configHandler', got %v", err) + // Then the service should be properly initialized + if virt == nil { + t.Fatal("Expected Virt, got nil") } }) } diff --git a/pkg/workstation/workstation.go b/pkg/workstation/workstation.go index 2e1d5b841..d41d11fe0 100644 --- a/pkg/workstation/workstation.go +++ b/pkg/workstation/workstation.go @@ -4,7 +4,6 @@ import ( "fmt" "os" - "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/runtime" "github.com/windsorcli/cli/pkg/runtime/shell/ssh" "github.com/windsorcli/cli/pkg/workstation/network" @@ -22,10 +21,10 @@ import ( // Types // ============================================================================= -// WorkstationRuntime holds the execution context for workstation operations. -// It embeds the base Runtime and includes all workstation-specific dependencies. -type WorkstationRuntime struct { - runtime.Runtime +// Workstation manages all workstation functionality including virtualization, +// networking, services, and SSH operations. +type Workstation struct { + *runtime.Runtime // Workstation-specific dependencies (created as needed) NetworkManager network.NetworkManager @@ -35,44 +34,49 @@ type WorkstationRuntime struct { SSHClient ssh.Client } -// Workstation manages all workstation functionality including virtualization, -// networking, services, and SSH operations. -// It embeds WorkstationRuntime so all fields are directly accessible. -type Workstation struct { - *WorkstationRuntime - injector di.Injector -} - // ============================================================================= // Constructor // ============================================================================= -// NewWorkstation creates a new Workstation instance with the provided execution context and injector. -// The execution context should already have ConfigHandler and Shell set. -// Other dependencies are created only if not already present on the context. -func NewWorkstation(ctx *WorkstationRuntime, injector di.Injector) (*Workstation, error) { - if ctx == nil { - return nil, fmt.Errorf("execution context is required") - } - if ctx.ConfigHandler == nil { - return nil, fmt.Errorf("ConfigHandler is required on execution context") +// NewWorkstation creates a new Workstation instance with the provided runtime. +// Other dependencies are created only if not already present via opts. +func NewWorkstation(rt *runtime.Runtime, opts ...*Workstation) (*Workstation, error) { + if rt == nil { + return nil, fmt.Errorf("runtime is required") } - if ctx.Shell == nil { - return nil, fmt.Errorf("Shell is required on execution context") + if rt.ConfigHandler == nil { + return nil, fmt.Errorf("ConfigHandler is required on runtime") } - if injector == nil { - return nil, fmt.Errorf("injector is required") + if rt.Shell == nil { + return nil, fmt.Errorf("Shell is required on runtime") } - // Create workstation first workstation := &Workstation{ - WorkstationRuntime: ctx, - injector: injector, + Runtime: rt, + } + + if len(opts) > 0 && opts[0] != nil { + overrides := opts[0] + if overrides.NetworkManager != nil { + workstation.NetworkManager = overrides.NetworkManager + } + if overrides.Services != nil { + workstation.Services = overrides.Services + } + if overrides.VirtualMachine != nil { + workstation.VirtualMachine = overrides.VirtualMachine + } + if overrides.ContainerRuntime != nil { + workstation.ContainerRuntime = overrides.ContainerRuntime + } + if overrides.SSHClient != nil { + workstation.SSHClient = overrides.SSHClient + } } // Create NetworkManager if not already set if workstation.NetworkManager == nil { - workstation.NetworkManager = network.NewBaseNetworkManager(injector) + workstation.NetworkManager = network.NewBaseNetworkManager(rt) } // Create Services if not already set @@ -88,7 +92,7 @@ func NewWorkstation(ctx *WorkstationRuntime, injector di.Injector) (*Workstation if workstation.VirtualMachine == nil { vmDriver := workstation.ConfigHandler.GetString("vm.driver", "") if vmDriver == "colima" { - workstation.VirtualMachine = virt.NewColimaVirt(injector) + workstation.VirtualMachine = virt.NewColimaVirt(rt) } } @@ -96,7 +100,7 @@ func NewWorkstation(ctx *WorkstationRuntime, injector di.Injector) (*Workstation if workstation.ContainerRuntime == nil { dockerEnabled := workstation.ConfigHandler.GetBool("docker.enabled", false) if dockerEnabled { - workstation.ContainerRuntime = virt.NewDockerVirt(injector) + workstation.ContainerRuntime = virt.NewDockerVirt(rt, workstation.Services) } } @@ -127,29 +131,11 @@ func (w *Workstation) Up() error { if w.VirtualMachine == nil { return fmt.Errorf("no virtual machine found") } - if err := w.VirtualMachine.Initialize(); err != nil { - return fmt.Errorf("failed to initialize virtual machine: %w", err) - } if err := w.VirtualMachine.Up(); err != nil { return fmt.Errorf("error running virtual machine Up command: %w", err) } } - if w.NetworkManager != nil { - if err := w.NetworkManager.Initialize(w.Services); err != nil { - return fmt.Errorf("failed to initialize network manager: %w", err) - } - } - - for _, service := range w.Services { - if dnsService, ok := service.(*services.DNSService); ok { - if err := dnsService.Initialize(); err != nil { - return fmt.Errorf("failed to re-initialize DNS service: %w", err) - } - break - } - } - for _, service := range w.Services { if err := service.WriteConfig(); err != nil { return fmt.Errorf("Error writing config for service %s: %w", service.GetName(), err) @@ -161,9 +147,6 @@ func (w *Workstation) Up() error { if w.ContainerRuntime == nil { return fmt.Errorf("no container runtime found") } - if err := w.ContainerRuntime.Initialize(); err != nil { - return fmt.Errorf("failed to initialize container runtime: %w", err) - } if err := w.ContainerRuntime.WriteConfig(); err != nil { return fmt.Errorf("failed to write container runtime config: %w", err) } @@ -200,18 +183,12 @@ func (w *Workstation) Up() error { // step fails, it returns an error describing the issue. func (w *Workstation) Down() error { if w.ContainerRuntime != nil { - if err := w.ContainerRuntime.Initialize(); err != nil { - return fmt.Errorf("failed to initialize container runtime: %w", err) - } if err := w.ContainerRuntime.Down(); err != nil { return fmt.Errorf("Error running container runtime Down command: %w", err) } } if w.VirtualMachine != nil { - if err := w.VirtualMachine.Initialize(); err != nil { - return fmt.Errorf("failed to initialize virtual machine: %w", err) - } if err := w.VirtualMachine.Down(); err != nil { return fmt.Errorf("Error running virtual machine Down command: %w", err) } @@ -238,36 +215,24 @@ func (w *Workstation) createServices() ([]services.Service, error) { // DNS Service dnsEnabled := w.ConfigHandler.GetBool("dns.enabled", false) if dnsEnabled { - service := services.NewDNSService(w.injector) + service := services.NewDNSService(w.Runtime) service.SetName("dns") - if err := service.Initialize(); err != nil { - return nil, fmt.Errorf("failed to initialize DNS service: %w", err) - } - w.injector.Register("dnsService", service) serviceList = append(serviceList, service) } // Git Livereload Service gitEnabled := w.ConfigHandler.GetBool("git.livereload.enabled", false) if gitEnabled { - service := services.NewGitLivereloadService(w.injector) + service := services.NewGitLivereloadService(w.Runtime) service.SetName("git") - if err := service.Initialize(); err != nil { - return nil, fmt.Errorf("failed to initialize Git Livereload service: %w", err) - } - w.injector.Register("gitLivereloadService", service) serviceList = append(serviceList, service) } // Localstack Service awsEnabled := w.ConfigHandler.GetBool("aws.localstack.enabled", false) if awsEnabled { - service := services.NewLocalstackService(w.injector) + service := services.NewLocalstackService(w.Runtime) service.SetName("aws") - if err := service.Initialize(); err != nil { - return nil, fmt.Errorf("failed to initialize Localstack service: %w", err) - } - w.injector.Register("localstackService", service) serviceList = append(serviceList, service) } @@ -275,12 +240,8 @@ func (w *Workstation) createServices() ([]services.Service, error) { contextConfig := w.ConfigHandler.GetConfig() if contextConfig.Docker != nil && contextConfig.Docker.Registries != nil { for key := range contextConfig.Docker.Registries { - service := services.NewRegistryService(w.injector) + service := services.NewRegistryService(w.Runtime) service.SetName(key) - if err := service.Initialize(); err != nil { - return nil, fmt.Errorf("failed to initialize Registry service %s: %w", key, err) - } - w.injector.Register(fmt.Sprintf("registryService.%s", key), service) serviceList = append(serviceList, service) } } @@ -293,27 +254,27 @@ func (w *Workstation) createServices() ([]services.Service, error) { workerCount := w.ConfigHandler.GetInt("cluster.workers.count") for i := 1; i <= controlPlaneCount; i++ { - service := services.NewTalosService(w.injector, "controlplane") + service := services.NewTalosService(w.Runtime, "controlplane") serviceName := fmt.Sprintf("controlplane-%d", i) service.SetName(serviceName) - if err := service.Initialize(); err != nil { - return nil, fmt.Errorf("failed to initialize Talos controlplane service %s: %w", serviceName, err) - } - w.injector.Register(fmt.Sprintf("talosService.%s", serviceName), service) serviceList = append(serviceList, service) } for i := 1; i <= workerCount; i++ { - service := services.NewTalosService(w.injector, "worker") + service := services.NewTalosService(w.Runtime, "worker") serviceName := fmt.Sprintf("worker-%d", i) service.SetName(serviceName) - if err := service.Initialize(); err != nil { - return nil, fmt.Errorf("failed to initialize Talos worker service %s: %w", serviceName, err) - } - w.injector.Register(fmt.Sprintf("talosService.%s", serviceName), service) serviceList = append(serviceList, service) } } + // Initialize DNS service with all services after they're all created + for _, service := range serviceList { + if dnsService, ok := service.(*services.DNSService); ok { + dnsService.SetServices(serviceList) + break + } + } + return serviceList, nil } diff --git a/pkg/workstation/workstation_test.go b/pkg/workstation/workstation_test.go index 547a0bd7a..b21acfe2a 100644 --- a/pkg/workstation/workstation_test.go +++ b/pkg/workstation/workstation_test.go @@ -12,7 +12,6 @@ import ( "github.com/windsorcli/cli/pkg/runtime/config" "github.com/windsorcli/cli/pkg/runtime/shell" "github.com/windsorcli/cli/pkg/runtime/shell/ssh" - "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/workstation/network" "github.com/windsorcli/cli/pkg/workstation/services" "github.com/windsorcli/cli/pkg/workstation/virt" @@ -23,7 +22,7 @@ import ( // ============================================================================= type Mocks struct { - Injector di.Injector + Runtime *ctxpkg.Runtime ConfigHandler config.ConfigHandler Shell *shell.MockShell NetworkManager *network.MockNetworkManager @@ -34,16 +33,21 @@ type Mocks struct { } type SetupOptions struct { - Injector di.Injector ConfigHandler config.ConfigHandler ConfigStr string } +func convertToServiceSlice(mockServices []*services.MockService) []services.Service { + serviceSlice := make([]services.Service, len(mockServices)) + for i, mockService := range mockServices { + serviceSlice[i] = mockService + } + return serviceSlice +} + func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { t.Helper() - // Create mock injector - mockInjector := di.NewMockInjector() // Create mock config handler mockConfigHandler := config.NewMockConfigHandler() @@ -161,6 +165,7 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { } // Set up mock network manager behaviors + mockNetworkManager.AssignIPsFunc = func(services []services.Service) error { return nil } mockNetworkManager.ConfigureHostRouteFunc = func() error { return nil } mockNetworkManager.ConfigureGuestFunc = func() error { return nil } mockNetworkManager.ConfigureDNSFunc = func() error { return nil } @@ -173,13 +178,6 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { mockContainerRuntime.UpFunc = func(verbose ...bool) error { return nil } mockContainerRuntime.DownFunc = func() error { return nil } - // Register mocks with injector - mockInjector.Register("configHandler", mockConfigHandler) - mockInjector.Register("shell", mockShell) - mockInjector.Register("networkManager", mockNetworkManager) - mockInjector.Register("virtualMachine", mockVirtualMachine) - mockInjector.Register("containerRuntime", mockContainerRuntime) - mockInjector.Register("sshClient", mockSSHClient) // Apply custom options if len(opts) > 0 && opts[0] != nil { @@ -190,8 +188,17 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { } } + rt := &ctxpkg.Runtime{ + ContextName: "test-context", + ProjectRoot: "/test/project", + ConfigRoot: "/test/project/contexts/test-context", + TemplateRoot: "/test/project/contexts/_template", + ConfigHandler: mockConfigHandler, + Shell: mockShell, + } + return &Mocks{ - Injector: mockInjector, + Runtime: rt, ConfigHandler: mockConfigHandler, Shell: mockShell, NetworkManager: mockNetworkManager, @@ -202,24 +209,6 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { } } -func setupWorkstationContext(mocks *Mocks) *WorkstationRuntime { - return &WorkstationRuntime{ - Runtime: ctxpkg.Runtime{ - ContextName: "test-context", - ProjectRoot: "/test/project", - ConfigRoot: "/test/project/contexts/test-context", - TemplateRoot: "/test/project/contexts/_template", - ConfigHandler: mocks.ConfigHandler, - Shell: mocks.Shell, - }, - NetworkManager: mocks.NetworkManager, - Services: []services.Service{mocks.Services[0], mocks.Services[1]}, - VirtualMachine: mocks.VirtualMachine, - ContainerRuntime: mocks.ContainerRuntime, - SSHClient: mocks.SSHClient, - } -} - // ============================================================================= // Test Constructor // ============================================================================= @@ -228,10 +217,9 @@ func TestNewWorkstation(t *testing.T) { t.Run("Success", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) // When - workstation, err := NewWorkstation(ctx, mocks.Injector) + workstation, err := NewWorkstation(mocks.Runtime) // Then if err != nil { @@ -250,10 +238,10 @@ func TestNewWorkstation(t *testing.T) { t.Run("NilContext", func(t *testing.T) { // Given - mocks := setupMocks(t) + _ = setupMocks(t) // When - workstation, err := NewWorkstation(nil, mocks.Injector) + workstation, err := NewWorkstation(nil) // Then if err == nil { @@ -262,7 +250,7 @@ func TestNewWorkstation(t *testing.T) { if workstation != nil { t.Error("Expected workstation to be nil") } - if err.Error() != "execution context is required" { + if err.Error() != "runtime is required" { t.Errorf("Expected specific error message, got: %v", err) } }) @@ -270,14 +258,12 @@ func TestNewWorkstation(t *testing.T) { t.Run("NilConfigHandler", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := &WorkstationRuntime{ - Runtime: ctxpkg.Runtime{ - Shell: mocks.Shell, - }, + rt := &ctxpkg.Runtime{ + Shell: mocks.Shell, } // When - workstation, err := NewWorkstation(ctx, mocks.Injector) + workstation, err := NewWorkstation(rt) // Then if err == nil { @@ -286,7 +272,7 @@ func TestNewWorkstation(t *testing.T) { if workstation != nil { t.Error("Expected workstation to be nil") } - if err.Error() != "ConfigHandler is required on execution context" { + if err.Error() != "ConfigHandler is required on runtime" { t.Errorf("Expected specific error message, got: %v", err) } }) @@ -294,14 +280,12 @@ func TestNewWorkstation(t *testing.T) { t.Run("NilShell", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := &WorkstationRuntime{ - Runtime: ctxpkg.Runtime{ - ConfigHandler: mocks.ConfigHandler, - }, + rt := &ctxpkg.Runtime{ + ConfigHandler: mocks.ConfigHandler, } // When - workstation, err := NewWorkstation(ctx, mocks.Injector) + workstation, err := NewWorkstation(rt) // Then if err == nil { @@ -310,18 +294,17 @@ func TestNewWorkstation(t *testing.T) { if workstation != nil { t.Error("Expected workstation to be nil") } - if err.Error() != "Shell is required on execution context" { + if err.Error() != "Shell is required on runtime" { t.Errorf("Expected specific error message, got: %v", err) } }) - t.Run("NilInjector", func(t *testing.T) { + t.Run("NilRuntime", func(t *testing.T) { // Given - mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) + _ = setupMocks(t) // When - workstation, err := NewWorkstation(ctx, nil) + workstation, err := NewWorkstation(nil) // Then if err == nil { @@ -330,7 +313,7 @@ func TestNewWorkstation(t *testing.T) { if workstation != nil { t.Error("Expected workstation to be nil") } - if err.Error() != "injector is required" { + if err.Error() != "runtime is required" { t.Errorf("Expected specific error message, got: %v", err) } }) @@ -338,10 +321,9 @@ func TestNewWorkstation(t *testing.T) { t.Run("CreatesDependencies", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) // When - workstation, err := NewWorkstation(ctx, mocks.Injector) + workstation, err := NewWorkstation(mocks.Runtime) // Then if err != nil { @@ -367,15 +349,17 @@ func TestNewWorkstation(t *testing.T) { t.Run("UsesExistingDependencies", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - ctx.NetworkManager = mocks.NetworkManager - ctx.Services = []services.Service{mocks.Services[0]} - ctx.VirtualMachine = mocks.VirtualMachine - ctx.ContainerRuntime = mocks.ContainerRuntime - ctx.SSHClient = mocks.SSHClient + opts := &Workstation{ + Runtime: mocks.Runtime, + NetworkManager: mocks.NetworkManager, + Services: []services.Service{mocks.Services[0]}, + VirtualMachine: mocks.VirtualMachine, + ContainerRuntime: mocks.ContainerRuntime, + SSHClient: mocks.SSHClient, + } // When - workstation, err := NewWorkstation(ctx, mocks.Injector) + workstation, err := NewWorkstation(mocks.Runtime, opts) // Then if err != nil { @@ -407,8 +391,12 @@ func TestWorkstation_Up(t *testing.T) { t.Run("Success", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) + workstation, err := NewWorkstation(mocks.Runtime, &Workstation{ + VirtualMachine: mocks.VirtualMachine, + ContainerRuntime: mocks.ContainerRuntime, + NetworkManager: mocks.NetworkManager, + Services: convertToServiceSlice(mocks.Services), + }) if err != nil { t.Fatalf("Failed to create workstation: %v", err) } @@ -425,8 +413,12 @@ func TestWorkstation_Up(t *testing.T) { t.Run("SetsNoCacheEnvironmentVariable", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) + workstation, err := NewWorkstation(mocks.Runtime, &Workstation{ + VirtualMachine: mocks.VirtualMachine, + ContainerRuntime: mocks.ContainerRuntime, + NetworkManager: mocks.NetworkManager, + Services: convertToServiceSlice(mocks.Services), + }) if err != nil { t.Fatalf("Failed to create workstation: %v", err) } @@ -446,17 +438,20 @@ func TestWorkstation_Up(t *testing.T) { t.Run("StartsVirtualMachine", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) - if err != nil { - t.Fatalf("Failed to create workstation: %v", err) - } - vmUpCalled := false mocks.VirtualMachine.UpFunc = func(verbose ...bool) error { vmUpCalled = true return nil } + workstation, err := NewWorkstation(mocks.Runtime, &Workstation{ + VirtualMachine: mocks.VirtualMachine, + ContainerRuntime: mocks.ContainerRuntime, + NetworkManager: mocks.NetworkManager, + Services: convertToServiceSlice(mocks.Services), + }) + if err != nil { + t.Fatalf("Failed to create workstation: %v", err) + } // When err = workstation.Up() @@ -473,17 +468,20 @@ func TestWorkstation_Up(t *testing.T) { t.Run("StartsContainerRuntime", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) - if err != nil { - t.Fatalf("Failed to create workstation: %v", err) - } - containerUpCalled := false mocks.ContainerRuntime.UpFunc = func(verbose ...bool) error { containerUpCalled = true return nil } + workstation, err := NewWorkstation(mocks.Runtime, &Workstation{ + VirtualMachine: mocks.VirtualMachine, + ContainerRuntime: mocks.ContainerRuntime, + NetworkManager: mocks.NetworkManager, + Services: convertToServiceSlice(mocks.Services), + }) + if err != nil { + t.Fatalf("Failed to create workstation: %v", err) + } // When err = workstation.Up() @@ -500,12 +498,6 @@ func TestWorkstation_Up(t *testing.T) { t.Run("ConfiguresNetworking", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) - if err != nil { - t.Fatalf("Failed to create workstation: %v", err) - } - hostRouteCalled := false guestCalled := false dnsCalled := false @@ -522,6 +514,15 @@ func TestWorkstation_Up(t *testing.T) { dnsCalled = true return nil } + workstation, err := NewWorkstation(mocks.Runtime, &Workstation{ + VirtualMachine: mocks.VirtualMachine, + ContainerRuntime: mocks.ContainerRuntime, + NetworkManager: mocks.NetworkManager, + Services: convertToServiceSlice(mocks.Services), + }) + if err != nil { + t.Fatalf("Failed to create workstation: %v", err) + } // When err = workstation.Up() @@ -544,12 +545,6 @@ func TestWorkstation_Up(t *testing.T) { t.Run("WritesServiceConfigs", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) - if err != nil { - t.Fatalf("Failed to create workstation: %v", err) - } - writeConfigCalled := false for _, service := range mocks.Services { service.WriteConfigFunc = func() error { @@ -557,6 +552,15 @@ func TestWorkstation_Up(t *testing.T) { return nil } } + workstation, err := NewWorkstation(mocks.Runtime, &Workstation{ + VirtualMachine: mocks.VirtualMachine, + ContainerRuntime: mocks.ContainerRuntime, + NetworkManager: mocks.NetworkManager, + Services: convertToServiceSlice(mocks.Services), + }) + if err != nil { + t.Fatalf("Failed to create workstation: %v", err) + } // When err = workstation.Up() @@ -573,15 +577,18 @@ func TestWorkstation_Up(t *testing.T) { t.Run("VirtualMachineUpError", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) - if err != nil { - t.Fatalf("Failed to create workstation: %v", err) - } - mocks.VirtualMachine.UpFunc = func(verbose ...bool) error { return fmt.Errorf("VM start failed") } + workstation, err := NewWorkstation(mocks.Runtime, &Workstation{ + VirtualMachine: mocks.VirtualMachine, + ContainerRuntime: mocks.ContainerRuntime, + NetworkManager: mocks.NetworkManager, + Services: convertToServiceSlice(mocks.Services), + }) + if err != nil { + t.Fatalf("Failed to create workstation: %v", err) + } // When err = workstation.Up() @@ -598,15 +605,18 @@ func TestWorkstation_Up(t *testing.T) { t.Run("ContainerRuntimeUpError", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) - if err != nil { - t.Fatalf("Failed to create workstation: %v", err) - } - mocks.ContainerRuntime.UpFunc = func(verbose ...bool) error { return fmt.Errorf("container start failed") } + workstation, err := NewWorkstation(mocks.Runtime, &Workstation{ + VirtualMachine: mocks.VirtualMachine, + ContainerRuntime: mocks.ContainerRuntime, + NetworkManager: mocks.NetworkManager, + Services: convertToServiceSlice(mocks.Services), + }) + if err != nil { + t.Fatalf("Failed to create workstation: %v", err) + } // When err = workstation.Up() @@ -623,15 +633,18 @@ func TestWorkstation_Up(t *testing.T) { t.Run("NetworkConfigurationError", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) - if err != nil { - t.Fatalf("Failed to create workstation: %v", err) - } - mocks.NetworkManager.ConfigureHostRouteFunc = func() error { return fmt.Errorf("network config failed") } + workstation, err := NewWorkstation(mocks.Runtime, &Workstation{ + VirtualMachine: mocks.VirtualMachine, + ContainerRuntime: mocks.ContainerRuntime, + NetworkManager: mocks.NetworkManager, + Services: convertToServiceSlice(mocks.Services), + }) + if err != nil { + t.Fatalf("Failed to create workstation: %v", err) + } // When err = workstation.Up() @@ -648,17 +661,20 @@ func TestWorkstation_Up(t *testing.T) { t.Run("ServiceWriteConfigError", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) - if err != nil { - t.Fatalf("Failed to create workstation: %v", err) - } - for _, service := range mocks.Services { service.WriteConfigFunc = func() error { return fmt.Errorf("service config failed") } } + workstation, err := NewWorkstation(mocks.Runtime, &Workstation{ + VirtualMachine: mocks.VirtualMachine, + ContainerRuntime: mocks.ContainerRuntime, + NetworkManager: mocks.NetworkManager, + Services: convertToServiceSlice(mocks.Services), + }) + if err != nil { + t.Fatalf("Failed to create workstation: %v", err) + } // When err = workstation.Up() @@ -677,8 +693,7 @@ func TestWorkstation_Down(t *testing.T) { t.Run("Success", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) + workstation, err := NewWorkstation(mocks.Runtime) if err != nil { t.Fatalf("Failed to create workstation: %v", err) } @@ -695,17 +710,17 @@ func TestWorkstation_Down(t *testing.T) { t.Run("StopsContainerRuntime", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) - if err != nil { - t.Fatalf("Failed to create workstation: %v", err) - } - containerDownCalled := false mocks.ContainerRuntime.DownFunc = func() error { containerDownCalled = true return nil } + workstation, err := NewWorkstation(mocks.Runtime, &Workstation{ + ContainerRuntime: mocks.ContainerRuntime, + }) + if err != nil { + t.Fatalf("Failed to create workstation: %v", err) + } // When err = workstation.Down() @@ -722,17 +737,17 @@ func TestWorkstation_Down(t *testing.T) { t.Run("StopsVirtualMachine", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) - if err != nil { - t.Fatalf("Failed to create workstation: %v", err) - } - vmDownCalled := false mocks.VirtualMachine.DownFunc = func() error { vmDownCalled = true return nil } + workstation, err := NewWorkstation(mocks.Runtime, &Workstation{ + VirtualMachine: mocks.VirtualMachine, + }) + if err != nil { + t.Fatalf("Failed to create workstation: %v", err) + } // When err = workstation.Down() @@ -749,15 +764,15 @@ func TestWorkstation_Down(t *testing.T) { t.Run("ContainerRuntimeDownError", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) - if err != nil { - t.Fatalf("Failed to create workstation: %v", err) - } - mocks.ContainerRuntime.DownFunc = func() error { return fmt.Errorf("container stop failed") } + workstation, err := NewWorkstation(mocks.Runtime, &Workstation{ + ContainerRuntime: mocks.ContainerRuntime, + }) + if err != nil { + t.Fatalf("Failed to create workstation: %v", err) + } // When err = workstation.Down() @@ -774,15 +789,15 @@ func TestWorkstation_Down(t *testing.T) { t.Run("VirtualMachineDownError", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) - if err != nil { - t.Fatalf("Failed to create workstation: %v", err) - } - mocks.VirtualMachine.DownFunc = func() error { return fmt.Errorf("VM stop failed") } + workstation, err := NewWorkstation(mocks.Runtime, &Workstation{ + VirtualMachine: mocks.VirtualMachine, + }) + if err != nil { + t.Fatalf("Failed to create workstation: %v", err) + } // When err = workstation.Down() @@ -805,8 +820,7 @@ func TestWorkstation_createServices(t *testing.T) { t.Run("Success", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) + workstation, err := NewWorkstation(mocks.Runtime) if err != nil { t.Fatalf("Failed to create workstation: %v", err) } @@ -836,9 +850,8 @@ func TestWorkstation_createServices(t *testing.T) { } return false } - ctx := setupWorkstationContext(mocks) - ctx.ConfigHandler = mockConfig - workstation, err := NewWorkstation(ctx, mocks.Injector) + mocks.Runtime.ConfigHandler = mockConfig + workstation, err := NewWorkstation(mocks.Runtime) if err != nil { t.Fatalf("Failed to create workstation: %v", err) } @@ -858,8 +871,7 @@ func TestWorkstation_createServices(t *testing.T) { t.Run("ServiceInitializationError", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) + workstation, err := NewWorkstation(mocks.Runtime) if err != nil { t.Fatalf("Failed to create workstation: %v", err) } @@ -879,8 +891,7 @@ func TestWorkstation_createServices(t *testing.T) { t.Run("CreatesDNSService", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) + workstation, err := NewWorkstation(mocks.Runtime) if err != nil { t.Fatalf("Failed to create workstation: %v", err) } @@ -900,8 +911,7 @@ func TestWorkstation_createServices(t *testing.T) { t.Run("CreatesGitLivereloadService", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) + workstation, err := NewWorkstation(mocks.Runtime) if err != nil { t.Fatalf("Failed to create workstation: %v", err) } @@ -921,8 +931,7 @@ func TestWorkstation_createServices(t *testing.T) { t.Run("CreatesLocalstackService", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) + workstation, err := NewWorkstation(mocks.Runtime) if err != nil { t.Fatalf("Failed to create workstation: %v", err) } @@ -942,8 +951,7 @@ func TestWorkstation_createServices(t *testing.T) { t.Run("CreatesRegistryServices", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) + workstation, err := NewWorkstation(mocks.Runtime) if err != nil { t.Fatalf("Failed to create workstation: %v", err) } @@ -963,8 +971,7 @@ func TestWorkstation_createServices(t *testing.T) { t.Run("CreatesTalosServices", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) + workstation, err := NewWorkstation(mocks.Runtime) if err != nil { t.Fatalf("Failed to create workstation: %v", err) } @@ -990,8 +997,12 @@ func TestWorkstation_Integration(t *testing.T) { t.Run("FullUpDownCycle", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) + workstation, err := NewWorkstation(mocks.Runtime, &Workstation{ + VirtualMachine: mocks.VirtualMachine, + ContainerRuntime: mocks.ContainerRuntime, + NetworkManager: mocks.NetworkManager, + Services: convertToServiceSlice(mocks.Services), + }) if err != nil { t.Fatalf("Failed to create workstation: %v", err) } @@ -1016,8 +1027,12 @@ func TestWorkstation_Integration(t *testing.T) { t.Run("MultipleUpDownCycles", func(t *testing.T) { // Given mocks := setupMocks(t) - ctx := setupWorkstationContext(mocks) - workstation, err := NewWorkstation(ctx, mocks.Injector) + workstation, err := NewWorkstation(mocks.Runtime, &Workstation{ + VirtualMachine: mocks.VirtualMachine, + ContainerRuntime: mocks.ContainerRuntime, + NetworkManager: mocks.NetworkManager, + Services: convertToServiceSlice(mocks.Services), + }) if err != nil { t.Fatalf("Failed to create workstation: %v", err) } From 3e1f57ffd3cdfad04e39eb8cb46c69227160e9f7 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:13:29 -0500 Subject: [PATCH 2/4] spaces Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/workstation/network/network.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/workstation/network/network.go b/pkg/workstation/network/network.go index 321a390f9..a7c86d927 100644 --- a/pkg/workstation/network/network.go +++ b/pkg/workstation/network/network.go @@ -32,15 +32,15 @@ type NetworkManager interface { // BaseNetworkManager is a concrete implementation of NetworkManager type BaseNetworkManager struct { - runtime *runtime.Runtime - sshClient ssh.Client - shell shell.Shell - secureShell shell.Shell - configHandler config.ConfigHandler - services []services.Service - shims *Shims - networkInterfaceProvider NetworkInterfaceProvider - portAllocator *services.PortAllocator + runtime *runtime.Runtime + sshClient ssh.Client + shell shell.Shell + secureShell shell.Shell + configHandler config.ConfigHandler + services []services.Service + shims *Shims + networkInterfaceProvider NetworkInterfaceProvider + portAllocator *services.PortAllocator } // ============================================================================= From d67a050c96d2dbf75ef8368a82548761cf6628a0 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:19:35 -0500 Subject: [PATCH 3/4] fix gosec Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/workstation/network/colima_network.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/workstation/network/colima_network.go b/pkg/workstation/network/colima_network.go index ad4257658..075d269a8 100644 --- a/pkg/workstation/network/colima_network.go +++ b/pkg/workstation/network/colima_network.go @@ -41,7 +41,8 @@ func NewColimaNetworkManager(rt *runtime.Runtime, sshClient ssh.Client, secureSh // Set docker.NetworkCIDR to the default value if it's not set if manager.configHandler.GetString("network.cidr_block") == "" { - manager.configHandler.Set("network.cidr_block", constants.DefaultNetworkCIDR) + // #nosec G104 - Constructor cannot return error; default setting is best-effort + _ = manager.configHandler.Set("network.cidr_block", constants.DefaultNetworkCIDR) } return manager From 211bf2e2729e6abfff9f2ec9aa37b0ceaef95aa8 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:26:00 -0500 Subject: [PATCH 4/4] Fix gosec Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/workstation/network/colima_network.go | 11 +---------- pkg/workstation/network/colima_network_test.go | 10 +++------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/pkg/workstation/network/colima_network.go b/pkg/workstation/network/colima_network.go index 075d269a8..06ab8589f 100644 --- a/pkg/workstation/network/colima_network.go +++ b/pkg/workstation/network/colima_network.go @@ -39,12 +39,6 @@ func NewColimaNetworkManager(rt *runtime.Runtime, sshClient ssh.Client, secureSh manager.sshClient = sshClient manager.secureShell = secureShell - // Set docker.NetworkCIDR to the default value if it's not set - if manager.configHandler.GetString("network.cidr_block") == "" { - // #nosec G104 - Constructor cannot return error; default setting is best-effort - _ = manager.configHandler.Set("network.cidr_block", constants.DefaultNetworkCIDR) - } - return manager } @@ -53,10 +47,7 @@ func NewColimaNetworkManager(rt *runtime.Runtime, sshClient ssh.Client, secureSh // It identifies the Docker bridge interface and ensures iptables rules are set. // If the rule doesn't exist, it adds a new one to allow traffic forwarding. func (n *ColimaNetworkManager) ConfigureGuest() error { - networkCIDR := n.configHandler.GetString("network.cidr_block") - if networkCIDR == "" { - return fmt.Errorf("network CIDR is not configured") - } + networkCIDR := n.configHandler.GetString("network.cidr_block", constants.DefaultNetworkCIDR) guestIP := n.configHandler.GetString("vm.address") if guestIP == "" { diff --git a/pkg/workstation/network/colima_network_test.go b/pkg/workstation/network/colima_network_test.go index e15cbe460..fc6df2bc4 100644 --- a/pkg/workstation/network/colima_network_test.go +++ b/pkg/workstation/network/colima_network_test.go @@ -72,13 +72,9 @@ func TestColimaNetworkManager_ConfigureGuest(t *testing.T) { // And configuring the guest err := manager.ConfigureGuest() - // Then an error should occur - if err == nil { - t.Fatalf("expected error, got nil") - } - expectedError := "network CIDR is not configured" - if err.Error() != expectedError { - t.Fatalf("expected error %q, got %q", expectedError, err.Error()) + // Then no error should occur (default CIDR is used automatically) + if err != nil { + t.Fatalf("expected no error, got %v", err) } })