diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index 454758f11..167b7e755 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -18,6 +18,8 @@ import ( "github.com/windsorcli/cli/pkg/shell/ssh" "github.com/windsorcli/cli/pkg/terraform" "github.com/windsorcli/cli/pkg/tools" + "github.com/windsorcli/cli/pkg/types" + "github.com/windsorcli/cli/pkg/workstation" "github.com/windsorcli/cli/pkg/workstation/network" "github.com/windsorcli/cli/pkg/workstation/services" "github.com/windsorcli/cli/pkg/workstation/virt" @@ -370,10 +372,97 @@ func (r *Runtime) ProcessArtifacts(opts ArtifactOptions) *Runtime { return r } +// WorkstationUp starts the workstation environment, including VMs, containers, networking, and services. +// It returns the Runtime instance, propagating any errors encountered during workstation initialization or startup. +// This method should be called after configuration, shell, and dependencies are properly loaded. +func (r *Runtime) WorkstationUp() *Runtime { + if r.err != nil { + return r + } + ws, err := r.createWorkstation() + if err != nil { + r.err = err + return r + } + if err := ws.Up(); err != nil { + r.err = fmt.Errorf("failed to start workstation: %w", err) + return r + } + return r +} + +// WorkstationDown stops the workstation environment, ensuring all services, containers, VMs, and associated networking are gracefully shut down. +// It returns the Runtime instance, propagating any errors encountered during the stopping process. +// This method should be invoked after workstation operations are complete and a teardown is required. +func (r *Runtime) WorkstationDown() *Runtime { + if r.err != nil { + return r + } + + ws, err := r.createWorkstation() + if err != nil { + r.err = err + return r + } + + if err := ws.Down(); err != nil { + r.err = fmt.Errorf("failed to stop workstation: %w", err) + return r + } + + return r +} + // ============================================================================= // Private Methods // ============================================================================= +// createWorkstation creates and initializes a workstation instance with the correct execution context. +// It validates that all required dependencies (ConfigHandler, Shell, Injector) are loaded, retrieves the current context, +// obtains the project root, and assembles an ExecutionContext for workstation operations. It returns a newly created +// workstation.Workstation or an error if any setup step fails. This method is used internally by both WorkstationUp and WorkstationDown. +func (r *Runtime) createWorkstation() (*workstation.Workstation, error) { + if r.ConfigHandler == nil { + return nil, fmt.Errorf("config handler not loaded - call LoadConfig() first") + } + if r.Shell == nil { + return nil, fmt.Errorf("shell not loaded - call LoadShell() first") + } + if r.Injector == nil { + return nil, fmt.Errorf("injector not available") + } + + contextName := r.ConfigHandler.GetContext() + if contextName == "" { + return nil, fmt.Errorf("no context set - call SetContext() first") + } + + projectRoot, err := r.Shell.GetProjectRoot() + if err != nil { + return nil, fmt.Errorf("failed to get project root: %w", err) + } + + execCtx := &types.ExecutionContext{ + ContextName: contextName, + ProjectRoot: projectRoot, + ConfigRoot: fmt.Sprintf("%s/contexts/%s", projectRoot, contextName), + TemplateRoot: fmt.Sprintf("%s/contexts/_template", projectRoot), + ConfigHandler: r.ConfigHandler, + Shell: r.Shell, + } + + workstationCtx := &workstation.WorkstationExecutionContext{ + ExecutionContext: *execCtx, + } + + ws, err := workstation.NewWorkstation(workstationCtx, r.Injector) + if err != nil { + return nil, fmt.Errorf("failed to create workstation: %w", err) + } + + return ws, nil +} + // getAllEnvPrinters returns all environment printers in field order, ensuring WindsorEnv is last. // This method provides compile-time structure assertions by mirroring the struct layout definition. // Panics at runtime if WindsorEnv is not last to guarantee environment variable precedence. diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go index f85d5d8f8..0e205407e 100644 --- a/pkg/runtime/runtime_test.go +++ b/pkg/runtime/runtime_test.go @@ -1687,3 +1687,517 @@ func TestRuntime_getAllEnvPrinters(t *testing.T) { } }) } + +func TestRuntime_WorkstationUp(t *testing.T) { + t.Run("StartsWorkstationSuccessfully", func(t *testing.T) { + // Given a runtime with loaded dependencies + mocks := setupMocks(t) + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { + return "test-context" + } + mocks.Shell.(*shell.MockShell).GetProjectRootFunc = func() (string, error) { + return "/test/project", nil + } + runtime := NewRuntime(mocks).LoadShell().LoadConfig() + + // When starting workstation + result := runtime.WorkstationUp() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected WorkstationUp to return the same runtime instance") + } + + // And error should be set due to workstation creation failure + // (This is expected since we don't have a real workstation setup) + if runtime.err == nil { + t.Error("Expected error to be set due to workstation creation failure") + } + }) + + t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { + // Given a runtime with an existing error + mocks := setupMocks(t) + runtime := NewRuntime(mocks) + expectedError := errors.New("existing error") + runtime.err = expectedError + + // When starting workstation + result := runtime.WorkstationUp() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected WorkstationUp to return the same runtime instance") + } + + // And error should remain unchanged + if runtime.err != expectedError { + t.Errorf("Expected error %v, got %v", expectedError, runtime.err) + } + }) + + t.Run("ReturnsErrorWhenConfigHandlerNotLoaded", func(t *testing.T) { + // Given a runtime without loaded config handler + runtime := NewRuntime() + + // When starting workstation + result := runtime.WorkstationUp() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected WorkstationUp to return the same runtime instance") + } + + // And error should be set + if runtime.err == nil { + t.Error("Expected error to be set") + } else { + expectedError := "config handler not loaded" + if !strings.Contains(runtime.err.Error(), expectedError) { + t.Errorf("Expected error to contain %q, got %q", expectedError, runtime.err.Error()) + } + } + }) + + t.Run("ReturnsErrorWhenShellNotLoaded", func(t *testing.T) { + // Given a runtime with config handler but no shell + mocks := setupMocks(t) + runtime := NewRuntime(mocks) + // Manually set config handler without calling LoadConfig (which loads shell) + runtime.ConfigHandler = mocks.ConfigHandler + // Explicitly set shell to nil + runtime.Shell = nil + + // When starting workstation + result := runtime.WorkstationUp() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected WorkstationUp to return the same runtime instance") + } + + // And error should be set + if runtime.err == nil { + t.Error("Expected error to be set") + } else { + expectedError := "shell not loaded" + if !strings.Contains(runtime.err.Error(), expectedError) { + t.Errorf("Expected error to contain %q, got %q", expectedError, runtime.err.Error()) + } + } + }) + + t.Run("ReturnsErrorWhenInjectorNotAvailable", func(t *testing.T) { + // Given a runtime with loaded dependencies but no injector + mocks := setupMocks(t) + runtime := NewRuntime(mocks).LoadShell().LoadConfig() + runtime.Injector = nil + + // When starting workstation + result := runtime.WorkstationUp() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected WorkstationUp to return the same runtime instance") + } + + // And error should be set + if runtime.err == nil { + t.Error("Expected error to be set") + } else { + expectedError := "injector not available" + if !strings.Contains(runtime.err.Error(), expectedError) { + t.Errorf("Expected error to contain %q, got %q", expectedError, runtime.err.Error()) + } + } + }) + + t.Run("ReturnsErrorWhenNoContextSet", func(t *testing.T) { + // Given a runtime with loaded dependencies but no context + mocks := setupMocks(t) + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { + return "" + } + runtime := NewRuntime(mocks).LoadShell().LoadConfig() + + // When starting workstation + result := runtime.WorkstationUp() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected WorkstationUp to return the same runtime instance") + } + + // And error should be set + if runtime.err == nil { + t.Error("Expected error to be set") + } else { + expectedError := "no context set" + if !strings.Contains(runtime.err.Error(), expectedError) { + t.Errorf("Expected error to contain %q, got %q", expectedError, runtime.err.Error()) + } + } + }) + + t.Run("PropagatesProjectRootError", func(t *testing.T) { + // Given a runtime with loaded dependencies + mocks := setupMocks(t) + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { + return "test-context" + } + expectedError := errors.New("project root error") + mocks.Shell.(*shell.MockShell).GetProjectRootFunc = func() (string, error) { + return "", expectedError + } + runtime := NewRuntime(mocks).LoadShell().LoadConfig() + + // When starting workstation + result := runtime.WorkstationUp() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected WorkstationUp to return the same runtime instance") + } + + // And error should be propagated + if runtime.err == nil { + t.Error("Expected error to be propagated") + } else { + expectedErrorText := "failed to get project root" + if !strings.Contains(runtime.err.Error(), expectedErrorText) { + t.Errorf("Expected error to contain %q, got %q", expectedErrorText, runtime.err.Error()) + } + } + }) +} + +func TestRuntime_WorkstationDown(t *testing.T) { + t.Run("StopsWorkstationSuccessfully", func(t *testing.T) { + // Given a runtime with loaded dependencies + mocks := setupMocks(t) + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { + return "test-context" + } + mocks.Shell.(*shell.MockShell).GetProjectRootFunc = func() (string, error) { + return "/test/project", nil + } + runtime := NewRuntime(mocks).LoadShell().LoadConfig() + + // When stopping workstation + result := runtime.WorkstationDown() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected WorkstationDown to return the same runtime instance") + } + + // And no error should be set (workstation down succeeds even with minimal setup) + if runtime.err != nil { + t.Errorf("Expected no error, got %v", runtime.err) + } + }) + + t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { + // Given a runtime with an existing error + mocks := setupMocks(t) + runtime := NewRuntime(mocks) + expectedError := errors.New("existing error") + runtime.err = expectedError + + // When stopping workstation + result := runtime.WorkstationDown() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected WorkstationDown to return the same runtime instance") + } + + // And error should remain unchanged + if runtime.err != expectedError { + t.Errorf("Expected error %v, got %v", expectedError, runtime.err) + } + }) + + t.Run("ReturnsErrorWhenConfigHandlerNotLoaded", func(t *testing.T) { + // Given a runtime without loaded config handler + runtime := NewRuntime() + + // When stopping workstation + result := runtime.WorkstationDown() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected WorkstationDown to return the same runtime instance") + } + + // And error should be set + if runtime.err == nil { + t.Error("Expected error to be set") + } else { + expectedError := "config handler not loaded" + if !strings.Contains(runtime.err.Error(), expectedError) { + t.Errorf("Expected error to contain %q, got %q", expectedError, runtime.err.Error()) + } + } + }) + + t.Run("ReturnsErrorWhenShellNotLoaded", func(t *testing.T) { + // Given a runtime with config handler but no shell + mocks := setupMocks(t) + runtime := NewRuntime(mocks) + // Manually set config handler without calling LoadConfig (which loads shell) + runtime.ConfigHandler = mocks.ConfigHandler + // Explicitly set shell to nil + runtime.Shell = nil + + // When stopping workstation + result := runtime.WorkstationDown() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected WorkstationDown to return the same runtime instance") + } + + // And error should be set + if runtime.err == nil { + t.Error("Expected error to be set") + } else { + expectedError := "shell not loaded" + if !strings.Contains(runtime.err.Error(), expectedError) { + t.Errorf("Expected error to contain %q, got %q", expectedError, runtime.err.Error()) + } + } + }) + + t.Run("ReturnsErrorWhenInjectorNotAvailable", func(t *testing.T) { + // Given a runtime with loaded dependencies but no injector + mocks := setupMocks(t) + runtime := NewRuntime(mocks).LoadShell().LoadConfig() + runtime.Injector = nil + + // When stopping workstation + result := runtime.WorkstationDown() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected WorkstationDown to return the same runtime instance") + } + + // And error should be set + if runtime.err == nil { + t.Error("Expected error to be set") + } else { + expectedError := "injector not available" + if !strings.Contains(runtime.err.Error(), expectedError) { + t.Errorf("Expected error to contain %q, got %q", expectedError, runtime.err.Error()) + } + } + }) + + t.Run("ReturnsErrorWhenNoContextSet", func(t *testing.T) { + // Given a runtime with loaded dependencies but no context + mocks := setupMocks(t) + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { + return "" + } + runtime := NewRuntime(mocks).LoadShell().LoadConfig() + + // When stopping workstation + result := runtime.WorkstationDown() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected WorkstationDown to return the same runtime instance") + } + + // And error should be set + if runtime.err == nil { + t.Error("Expected error to be set") + } else { + expectedError := "no context set" + if !strings.Contains(runtime.err.Error(), expectedError) { + t.Errorf("Expected error to contain %q, got %q", expectedError, runtime.err.Error()) + } + } + }) + + t.Run("PropagatesProjectRootError", func(t *testing.T) { + // Given a runtime with loaded dependencies + mocks := setupMocks(t) + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { + return "test-context" + } + expectedError := errors.New("project root error") + mocks.Shell.(*shell.MockShell).GetProjectRootFunc = func() (string, error) { + return "", expectedError + } + runtime := NewRuntime(mocks).LoadShell().LoadConfig() + + // When stopping workstation + result := runtime.WorkstationDown() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected WorkstationDown to return the same runtime instance") + } + + // And error should be propagated + if runtime.err == nil { + t.Error("Expected error to be propagated") + } else { + expectedErrorText := "failed to get project root" + if !strings.Contains(runtime.err.Error(), expectedErrorText) { + t.Errorf("Expected error to contain %q, got %q", expectedErrorText, runtime.err.Error()) + } + } + }) +} + +func TestRuntime_createWorkstation(t *testing.T) { + t.Run("CreatesWorkstationSuccessfully", func(t *testing.T) { + // Given a runtime with loaded dependencies + mocks := setupMocks(t) + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { + return "test-context" + } + mocks.Shell.(*shell.MockShell).GetProjectRootFunc = func() (string, error) { + return "/test/project", nil + } + runtime := NewRuntime(mocks).LoadShell().LoadConfig() + + // When creating workstation + ws, err := runtime.createWorkstation() + + // Then should return workstation and no error + if ws == nil { + t.Error("Expected workstation to be created") + } + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("ReturnsErrorWhenConfigHandlerNotLoaded", func(t *testing.T) { + // Given a runtime without loaded config handler + runtime := NewRuntime() + + // When creating workstation + ws, err := runtime.createWorkstation() + + // Then should return error + if ws != nil { + t.Error("Expected workstation to be nil") + } + if err == nil { + t.Error("Expected error to be returned") + } else { + expectedError := "config handler not loaded" + if !strings.Contains(err.Error(), expectedError) { + t.Errorf("Expected error to contain %q, got %q", expectedError, err.Error()) + } + } + }) + + t.Run("ReturnsErrorWhenShellNotLoaded", func(t *testing.T) { + // Given a runtime with config handler but no shell + mocks := setupMocks(t) + runtime := NewRuntime(mocks) + // Manually set config handler without calling LoadConfig (which loads shell) + runtime.ConfigHandler = mocks.ConfigHandler + // Explicitly set shell to nil + runtime.Shell = nil + + // When creating workstation + ws, err := runtime.createWorkstation() + + // Then should return error + if ws != nil { + t.Error("Expected workstation to be nil") + } + if err == nil { + t.Error("Expected error to be returned") + } else { + expectedError := "shell not loaded" + if !strings.Contains(err.Error(), expectedError) { + t.Errorf("Expected error to contain %q, got %q", expectedError, err.Error()) + } + } + }) + + t.Run("ReturnsErrorWhenInjectorNotAvailable", func(t *testing.T) { + // Given a runtime with loaded dependencies but no injector + mocks := setupMocks(t) + runtime := NewRuntime(mocks).LoadShell().LoadConfig() + runtime.Injector = nil + + // When creating workstation + ws, err := runtime.createWorkstation() + + // Then should return error + if ws != nil { + t.Error("Expected workstation to be nil") + } + if err == nil { + t.Error("Expected error to be returned") + } else { + expectedError := "injector not available" + if !strings.Contains(err.Error(), expectedError) { + t.Errorf("Expected error to contain %q, got %q", expectedError, err.Error()) + } + } + }) + + t.Run("ReturnsErrorWhenNoContextSet", func(t *testing.T) { + // Given a runtime with loaded dependencies but no context + mocks := setupMocks(t) + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { + return "" + } + runtime := NewRuntime(mocks).LoadShell().LoadConfig() + + // When creating workstation + ws, err := runtime.createWorkstation() + + // Then should return error + if ws != nil { + t.Error("Expected workstation to be nil") + } + if err == nil { + t.Error("Expected error to be returned") + } else { + expectedError := "no context set" + if !strings.Contains(err.Error(), expectedError) { + t.Errorf("Expected error to contain %q, got %q", expectedError, err.Error()) + } + } + }) + + t.Run("PropagatesProjectRootError", func(t *testing.T) { + // Given a runtime with loaded dependencies + mocks := setupMocks(t) + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { + return "test-context" + } + expectedError := errors.New("project root error") + mocks.Shell.(*shell.MockShell).GetProjectRootFunc = func() (string, error) { + return "", expectedError + } + runtime := NewRuntime(mocks).LoadShell().LoadConfig() + + // When creating workstation + ws, err := runtime.createWorkstation() + + // Then should return error + if ws != nil { + t.Error("Expected workstation to be nil") + } + if err == nil { + t.Error("Expected error to be returned") + } else { + expectedErrorText := "failed to get project root" + if !strings.Contains(err.Error(), expectedErrorText) { + t.Errorf("Expected error to contain %q, got %q", expectedErrorText, err.Error()) + } + } + }) +} diff --git a/pkg/runtime/shims.go b/pkg/runtime/shims.go index 99bcd1c13..6e64c2890 100644 --- a/pkg/runtime/shims.go +++ b/pkg/runtime/shims.go @@ -7,8 +7,10 @@ import "os" // dependency injection of mock implementations for file system and environment operations. // Each pipeline can use its own Shims instance with customized behavior for testing scenarios. type Shims struct { - Stat func(name string) (os.FileInfo, error) - Getenv func(key string) string + Stat func(name string) (os.FileInfo, error) + Getenv func(key string) string + Setenv func(key, value string) error + RemoveAll func(path string) error } // NewShims creates a new Shims instance with default system call implementations. @@ -16,7 +18,9 @@ type Shims struct { // used in production environments or as a base for creating test-specific variants. func NewShims() *Shims { return &Shims{ - Stat: os.Stat, - Getenv: os.Getenv, + Stat: os.Stat, + Getenv: os.Getenv, + Setenv: os.Setenv, + RemoveAll: os.RemoveAll, } } diff --git a/pkg/types/context.go b/pkg/types/context.go new file mode 100644 index 000000000..d02ba3ac8 --- /dev/null +++ b/pkg/types/context.go @@ -0,0 +1,26 @@ +package types + +import ( + "github.com/windsorcli/cli/pkg/config" + "github.com/windsorcli/cli/pkg/shell" +) + +// ExecutionContext holds common execution values and core dependencies used across the Windsor CLI. +// These fields are set during various initialization steps rather than computed on-demand. +type ExecutionContext struct { + // ContextName is the current context name + ContextName string + + // ProjectRoot is the project root directory path + ProjectRoot string + + // ConfigRoot is the config root directory (/contexts/) + ConfigRoot string + + // TemplateRoot is the template directory (/contexts/_template) + TemplateRoot string + + // Core dependencies + ConfigHandler config.ConfigHandler + Shell shell.Shell +} diff --git a/pkg/workstation/workstation.go b/pkg/workstation/workstation.go new file mode 100644 index 000000000..838f69cc6 --- /dev/null +++ b/pkg/workstation/workstation.go @@ -0,0 +1,280 @@ +package workstation + +import ( + "fmt" + "os" + + "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/shell/ssh" + "github.com/windsorcli/cli/pkg/types" + "github.com/windsorcli/cli/pkg/workstation/network" + "github.com/windsorcli/cli/pkg/workstation/services" + "github.com/windsorcli/cli/pkg/workstation/virt" +) + +// The Workstation is a core component that manages all workstation functionality including virtualization, +// networking, services, and SSH operations. +// It provides a unified interface for starting, stopping, and managing workstation infrastructure, +// The Workstation acts as the central workstation orchestrator for the application, +// coordinating VM lifecycle, container runtime management, network configuration, and service orchestration. + +// ============================================================================= +// Types +// ============================================================================= + +// WorkstationExecutionContext holds the execution context for workstation operations. +// It embeds the base ExecutionContext and includes all workstation-specific dependencies. +type WorkstationExecutionContext struct { + types.ExecutionContext + + // Workstation-specific dependencies (created as needed) + NetworkManager network.NetworkManager + Services []services.Service + VirtualMachine virt.VirtualMachine + ContainerRuntime virt.ContainerRuntime + SSHClient ssh.Client +} + +// Workstation manages all workstation functionality including virtualization, +// networking, services, and SSH operations. +// It embeds WorkstationExecutionContext so all fields are directly accessible. +type Workstation struct { + *WorkstationExecutionContext + 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 *WorkstationExecutionContext, 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") + } + if ctx.Shell == nil { + return nil, fmt.Errorf("Shell is required on execution context") + } + if injector == nil { + return nil, fmt.Errorf("injector is required") + } + + // Create workstation first + workstation := &Workstation{ + WorkstationExecutionContext: ctx, + injector: injector, + } + + // Create NetworkManager if not already set + if workstation.NetworkManager == nil { + workstation.NetworkManager = network.NewBaseNetworkManager(injector) + } + + // Create Services if not already set + if workstation.Services == nil { + services, err := workstation.createServices() + if err != nil { + return nil, fmt.Errorf("failed to create services: %w", err) + } + workstation.Services = services + } + + // Create VirtualMachine if not already set + if workstation.VirtualMachine == nil { + vmDriver := workstation.ConfigHandler.GetString("vm.driver", "") + if vmDriver == "colima" { + workstation.VirtualMachine = virt.NewColimaVirt(injector) + } + } + + // Create ContainerRuntime if not already set + if workstation.ContainerRuntime == nil { + dockerEnabled := workstation.ConfigHandler.GetBool("docker.enabled", false) + if dockerEnabled { + workstation.ContainerRuntime = virt.NewDockerVirt(injector) + } + } + + // Create SSHClient if not already set + if workstation.SSHClient == nil { + workstation.SSHClient = ssh.NewSSHClient() + } + + return workstation, nil +} + +// ============================================================================= +// Public Methods +// ============================================================================= + +// Up starts the workstation environment including VMs, containers, networking, and services. +func (w *Workstation) Up() error { + // Set NO_CACHE environment variable to prevent caching during up operations + if err := os.Setenv("NO_CACHE", "true"); err != nil { + return fmt.Errorf("Error setting NO_CACHE environment variable: %w", err) + } + + // Start virtual machine if using colima + vmDriver := w.ConfigHandler.GetString("vm.driver") + if vmDriver == "colima" { + if w.VirtualMachine == nil { + return fmt.Errorf("no virtual machine found") + } + if err := w.VirtualMachine.Up(); err != nil { + return fmt.Errorf("error running virtual machine Up command: %w", err) + } + } + + // Start container runtime if enabled + containerRuntimeEnabled := w.ConfigHandler.GetBool("docker.enabled") + if containerRuntimeEnabled { + if w.ContainerRuntime == nil { + return fmt.Errorf("no container runtime found") + } + if err := w.ContainerRuntime.Up(); err != nil { + return fmt.Errorf("error running container runtime Up command: %w", err) + } + } + + // Configure networking + if w.NetworkManager != nil { + if err := w.NetworkManager.ConfigureHostRoute(); err != nil { + return fmt.Errorf("error configuring host route: %w", err) + } + if err := w.NetworkManager.ConfigureGuest(); err != nil { + return fmt.Errorf("error configuring guest: %w", err) + } + if err := w.NetworkManager.ConfigureDNS(); err != nil { + return fmt.Errorf("error configuring DNS: %w", err) + } + } + + // Write service configurations + for _, service := range w.Services { + if err := service.WriteConfig(); err != nil { + return fmt.Errorf("Error writing config for service %s: %w", service.GetName(), err) + } + } + + // Print success message + fmt.Fprintln(os.Stderr, "Windsor environment set up successfully.") + + return nil +} + +// Down stops the workstation environment including services, containers, VMs, and networking. +func (w *Workstation) Down() error { + // Stop container runtime + if w.ContainerRuntime != nil { + if err := w.ContainerRuntime.Down(); err != nil { + return fmt.Errorf("Error running container runtime Down command: %w", err) + } + } + + // Stop virtual machine + if w.VirtualMachine != nil { + if err := w.VirtualMachine.Down(); err != nil { + return fmt.Errorf("Error running virtual machine Down command: %w", err) + } + } + + // Print success message + fmt.Fprintln(os.Stderr, "Windsor environment torn down successfully.") + + return nil +} + +// ============================================================================= +// Private Methods +// ============================================================================= + +// createServices creates services based on configuration settings. +func (w *Workstation) createServices() ([]services.Service, error) { + var serviceList []services.Service + + dockerEnabled := w.ConfigHandler.GetBool("docker.enabled", false) + if !dockerEnabled { + return serviceList, nil + } + + // DNS Service + dnsEnabled := w.ConfigHandler.GetBool("dns.enabled", false) + if dnsEnabled { + service := services.NewDNSService(w.injector) + service.SetName("dns") + if err := service.Initialize(); err != nil { + return nil, fmt.Errorf("failed to initialize DNS service: %w", err) + } + serviceList = append(serviceList, service) + } + + // Git Livereload Service + gitEnabled := w.ConfigHandler.GetBool("git.livereload.enabled", false) + if gitEnabled { + service := services.NewGitLivereloadService(w.injector) + service.SetName("git") + if err := service.Initialize(); err != nil { + return nil, fmt.Errorf("failed to initialize Git Livereload service: %w", err) + } + serviceList = append(serviceList, service) + } + + // Localstack Service + awsEnabled := w.ConfigHandler.GetBool("aws.localstack.enabled", false) + if awsEnabled { + service := services.NewLocalstackService(w.injector) + service.SetName("aws") + if err := service.Initialize(); err != nil { + return nil, fmt.Errorf("failed to initialize Localstack service: %w", err) + } + serviceList = append(serviceList, service) + } + + // Registry Services + contextConfig := w.ConfigHandler.GetConfig() + if contextConfig.Docker != nil && contextConfig.Docker.Registries != nil { + for key := range contextConfig.Docker.Registries { + service := services.NewRegistryService(w.injector) + service.SetName(key) + if err := service.Initialize(); err != nil { + return nil, fmt.Errorf("failed to initialize Registry service %s: %w", key, err) + } + serviceList = append(serviceList, service) + } + } + + // Cluster Services + clusterDriver := w.ConfigHandler.GetString("cluster.driver", "") + switch clusterDriver { + case "talos", "omni": + controlPlaneCount := w.ConfigHandler.GetInt("cluster.controlplanes.count") + workerCount := w.ConfigHandler.GetInt("cluster.workers.count") + + for i := 1; i <= controlPlaneCount; i++ { + service := services.NewTalosService(w.injector, "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) + } + serviceList = append(serviceList, service) + } + + for i := 1; i <= workerCount; i++ { + service := services.NewTalosService(w.injector, "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) + } + serviceList = append(serviceList, service) + } + } + + return serviceList, nil +} diff --git a/pkg/workstation/workstation_test.go b/pkg/workstation/workstation_test.go new file mode 100644 index 000000000..8ed726761 --- /dev/null +++ b/pkg/workstation/workstation_test.go @@ -0,0 +1,1038 @@ +package workstation + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/windsorcli/cli/api/v1alpha1" + "github.com/windsorcli/cli/api/v1alpha1/docker" + "github.com/windsorcli/cli/pkg/config" + "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/shell" + "github.com/windsorcli/cli/pkg/shell/ssh" + "github.com/windsorcli/cli/pkg/types" + "github.com/windsorcli/cli/pkg/workstation/network" + "github.com/windsorcli/cli/pkg/workstation/services" + "github.com/windsorcli/cli/pkg/workstation/virt" +) + +// ============================================================================= +// Test Setup +// ============================================================================= + +type Mocks struct { + Injector di.Injector + ConfigHandler config.ConfigHandler + Shell *shell.MockShell + NetworkManager *network.MockNetworkManager + Services []*services.MockService + VirtualMachine *virt.MockVirt + ContainerRuntime *virt.MockVirt + SSHClient *ssh.MockClient +} + +type SetupOptions struct { + Injector di.Injector + ConfigHandler config.ConfigHandler + ConfigStr string +} + +func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { + t.Helper() + + // Create mock injector + mockInjector := di.NewMockInjector() + + // Create mock config handler + mockConfigHandler := config.NewMockConfigHandler() + + // Create mock shell + mockShell := shell.NewMockShell() + + // Create mock network manager + mockNetworkManager := network.NewMockNetworkManager() + + // Create mock services + mockServices := []*services.MockService{ + services.NewMockService(), + services.NewMockService(), + } + + // Create mock virtual machine + mockVirtualMachine := virt.NewMockVirt() + + // Create mock container runtime + mockContainerRuntime := virt.NewMockVirt() + + // Create mock SSH client + mockSSHClient := &ssh.MockClient{} + + // Set up mock behaviors + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + switch key { + case "vm.driver": + return "colima" + case "docker.enabled": + return "true" + case "dns.enabled": + return "true" + case "git.livereload.enabled": + return "true" + case "aws.localstack.enabled": + return "true" + case "cluster.driver": + return "talos" + case "cluster.controlplanes.count": + return "2" + case "cluster.workers.count": + return "1" + default: + if len(defaultValue) > 0 { + return defaultValue[0] + } + return "" + } + } + + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + switch key { + case "docker.enabled": + return true + case "dns.enabled": + return true + case "git.livereload.enabled": + return true + case "aws.localstack.enabled": + return true + default: + if len(defaultValue) > 0 { + return defaultValue[0] + } + return false + } + } + + mockConfigHandler.GetIntFunc = func(key string, defaultValue ...int) int { + switch key { + case "cluster.controlplanes.count": + return 2 + case "cluster.workers.count": + return 1 + default: + if len(defaultValue) > 0 { + return defaultValue[0] + } + return 0 + } + } + + mockConfigHandler.GetStringSliceFunc = func(key string, defaultValue ...[]string) []string { + if len(defaultValue) > 0 { + return defaultValue[0] + } + return []string{} + } + + mockConfigHandler.GetConfigFunc = func() *v1alpha1.Context { + return &v1alpha1.Context{ + Docker: &docker.DockerConfig{ + Registries: map[string]docker.RegistryConfig{ + "test-registry": { + HostPort: 5000, + Remote: "https://registry.example.com", + }, + }, + }, + } + } + + mockShell.GetProjectRootFunc = func() (string, error) { + return "/test/project", nil + } + + // Set up mock service behaviors + for _, service := range mockServices { + service.SetNameFunc = func(name string) {} + service.GetNameFunc = func() string { return "test-service" } + service.WriteConfigFunc = func() error { return nil } + service.InitializeFunc = func() error { return nil } + } + + // Set up mock network manager behaviors + mockNetworkManager.ConfigureHostRouteFunc = func() error { return nil } + mockNetworkManager.ConfigureGuestFunc = func() error { return nil } + mockNetworkManager.ConfigureDNSFunc = func() error { return nil } + + // Set up mock virtual machine behaviors + mockVirtualMachine.UpFunc = func(verbose ...bool) error { return nil } + mockVirtualMachine.DownFunc = func() error { return nil } + + // Set up mock container runtime behaviors + 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 { + if opts[0].ConfigHandler != nil { + if mockConfig, ok := opts[0].ConfigHandler.(*config.MockConfigHandler); ok { + mockConfigHandler = mockConfig + } + } + } + + return &Mocks{ + Injector: mockInjector, + ConfigHandler: mockConfigHandler, + Shell: mockShell, + NetworkManager: mockNetworkManager, + Services: mockServices, + VirtualMachine: mockVirtualMachine, + ContainerRuntime: mockContainerRuntime, + SSHClient: mockSSHClient, + } +} + +func setupWorkstationContext(mocks *Mocks) *WorkstationExecutionContext { + return &WorkstationExecutionContext{ + ExecutionContext: types.ExecutionContext{ + 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 +// ============================================================================= + +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) + + // Then + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + if workstation == nil { + t.Error("Expected workstation to be created") + } + if workstation.ConfigHandler == nil { + t.Error("Expected ConfigHandler to be set") + } + if workstation.Shell == nil { + t.Error("Expected Shell to be set") + } + }) + + t.Run("NilContext", func(t *testing.T) { + // Given + mocks := setupMocks(t) + + // When + workstation, err := NewWorkstation(nil, mocks.Injector) + + // Then + if err == nil { + t.Error("Expected error for nil context") + } + if workstation != nil { + t.Error("Expected workstation to be nil") + } + if err.Error() != "execution context is required" { + t.Errorf("Expected specific error message, got: %v", err) + } + }) + + t.Run("NilConfigHandler", func(t *testing.T) { + // Given + mocks := setupMocks(t) + ctx := &WorkstationExecutionContext{ + ExecutionContext: types.ExecutionContext{ + Shell: mocks.Shell, + }, + } + + // When + workstation, err := NewWorkstation(ctx, mocks.Injector) + + // Then + if err == nil { + t.Error("Expected error for nil ConfigHandler") + } + if workstation != nil { + t.Error("Expected workstation to be nil") + } + if err.Error() != "ConfigHandler is required on execution context" { + t.Errorf("Expected specific error message, got: %v", err) + } + }) + + t.Run("NilShell", func(t *testing.T) { + // Given + mocks := setupMocks(t) + ctx := &WorkstationExecutionContext{ + ExecutionContext: types.ExecutionContext{ + ConfigHandler: mocks.ConfigHandler, + }, + } + + // When + workstation, err := NewWorkstation(ctx, mocks.Injector) + + // Then + if err == nil { + t.Error("Expected error for nil Shell") + } + if workstation != nil { + t.Error("Expected workstation to be nil") + } + if err.Error() != "Shell is required on execution context" { + t.Errorf("Expected specific error message, got: %v", err) + } + }) + + t.Run("NilInjector", func(t *testing.T) { + // Given + mocks := setupMocks(t) + ctx := setupWorkstationContext(mocks) + + // When + workstation, err := NewWorkstation(ctx, nil) + + // Then + if err == nil { + t.Error("Expected error for nil injector") + } + if workstation != nil { + t.Error("Expected workstation to be nil") + } + if err.Error() != "injector is required" { + t.Errorf("Expected specific error message, got: %v", err) + } + }) + + t.Run("CreatesDependencies", func(t *testing.T) { + // Given + mocks := setupMocks(t) + ctx := setupWorkstationContext(mocks) + + // When + workstation, err := NewWorkstation(ctx, mocks.Injector) + + // Then + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + if workstation.NetworkManager == nil { + t.Error("Expected NetworkManager to be created") + } + if workstation.Services == nil { + t.Error("Expected Services to be created") + } + if workstation.VirtualMachine == nil { + t.Error("Expected VirtualMachine to be created") + } + if workstation.ContainerRuntime == nil { + t.Error("Expected ContainerRuntime to be created") + } + if workstation.SSHClient == nil { + t.Error("Expected SSHClient to be created") + } + }) + + 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 + + // When + workstation, err := NewWorkstation(ctx, mocks.Injector) + + // Then + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + if workstation.NetworkManager != mocks.NetworkManager { + t.Error("Expected existing NetworkManager to be used") + } + if len(workstation.Services) != 1 { + t.Error("Expected existing Services to be used") + } + if workstation.VirtualMachine != mocks.VirtualMachine { + t.Error("Expected existing VirtualMachine to be used") + } + if workstation.ContainerRuntime != mocks.ContainerRuntime { + t.Error("Expected existing ContainerRuntime to be used") + } + if workstation.SSHClient != mocks.SSHClient { + t.Error("Expected existing SSHClient to be used") + } + }) +} + +// ============================================================================= +// Test Public Methods +// ============================================================================= + +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) + if err != nil { + t.Fatalf("Failed to create workstation: %v", err) + } + + // When + err = workstation.Up() + + // Then + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + }) + + t.Run("SetsNoCacheEnvironmentVariable", 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) + } + + // When + err = workstation.Up() + + // Then + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + if os.Getenv("NO_CACHE") != "true" { + t.Error("Expected NO_CACHE environment variable to be set") + } + }) + + 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 + } + + // When + err = workstation.Up() + + // Then + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + if !vmUpCalled { + t.Error("Expected VirtualMachine.Up to be called") + } + }) + + 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 + } + + // When + err = workstation.Up() + + // Then + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + if !containerUpCalled { + t.Error("Expected ContainerRuntime.Up to be called") + } + }) + + 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 + + mocks.NetworkManager.ConfigureHostRouteFunc = func() error { + hostRouteCalled = true + return nil + } + mocks.NetworkManager.ConfigureGuestFunc = func() error { + guestCalled = true + return nil + } + mocks.NetworkManager.ConfigureDNSFunc = func() error { + dnsCalled = true + return nil + } + + // When + err = workstation.Up() + + // Then + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + if !hostRouteCalled { + t.Error("Expected ConfigureHostRoute to be called") + } + if !guestCalled { + t.Error("Expected ConfigureGuest to be called") + } + if !dnsCalled { + t.Error("Expected ConfigureDNS to be called") + } + }) + + 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 { + writeConfigCalled = true + return nil + } + } + + // When + err = workstation.Up() + + // Then + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + if !writeConfigCalled { + t.Error("Expected service WriteConfig to be called") + } + }) + + 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") + } + + // When + err = workstation.Up() + + // Then + if err == nil { + t.Error("Expected error for VM start failure") + } + if !strings.Contains(err.Error(), "error running virtual machine Up command") { + t.Errorf("Expected specific error message, got: %v", err) + } + }) + + 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") + } + + // When + err = workstation.Up() + + // Then + if err == nil { + t.Error("Expected error for container start failure") + } + if !strings.Contains(err.Error(), "error running container runtime Up command") { + t.Errorf("Expected specific error message, got: %v", err) + } + }) + + 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") + } + + // When + err = workstation.Up() + + // Then + if err == nil { + t.Error("Expected error for network configuration failure") + } + if !strings.Contains(err.Error(), "error configuring host route") { + t.Errorf("Expected specific error message, got: %v", err) + } + }) + + 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") + } + } + + // When + err = workstation.Up() + + // Then + if err == nil { + t.Error("Expected error for service config failure") + } + if !strings.Contains(err.Error(), "Error writing config for service") { + t.Errorf("Expected specific error message, got: %v", err) + } + }) +} + +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) + if err != nil { + t.Fatalf("Failed to create workstation: %v", err) + } + + // When + err = workstation.Down() + + // Then + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + }) + + 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 + } + + // When + err = workstation.Down() + + // Then + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + if !containerDownCalled { + t.Error("Expected ContainerRuntime.Down to be called") + } + }) + + 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 + } + + // When + err = workstation.Down() + + // Then + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + if !vmDownCalled { + t.Error("Expected VirtualMachine.Down to be called") + } + }) + + 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") + } + + // When + err = workstation.Down() + + // Then + if err == nil { + t.Error("Expected error for container stop failure") + } + if !strings.Contains(err.Error(), "Error running container runtime Down command") { + t.Errorf("Expected specific error message, got: %v", err) + } + }) + + 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") + } + + // When + err = workstation.Down() + + // Then + if err == nil { + t.Error("Expected error for VM stop failure") + } + if !strings.Contains(err.Error(), "Error running virtual machine Down command") { + t.Errorf("Expected specific error message, got: %v", err) + } + }) +} + +// ============================================================================= +// Test Private Methods +// ============================================================================= + +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) + if err != nil { + t.Fatalf("Failed to create workstation: %v", err) + } + + // When + services, err := workstation.createServices() + + // Then + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + if services == nil { + t.Error("Expected services to be created") + } + if len(services) == 0 { + t.Error("Expected services to be created") + } + }) + + t.Run("DockerDisabled", func(t *testing.T) { + // Given + mocks := setupMocks(t) + mockConfig := config.NewMockConfigHandler() + mockConfig.GetBoolFunc = func(key string, defaultValue ...bool) bool { + if key == "docker.enabled" { + return false + } + return false + } + ctx := setupWorkstationContext(mocks) + ctx.ConfigHandler = mockConfig + workstation, err := NewWorkstation(ctx, mocks.Injector) + if err != nil { + t.Fatalf("Failed to create workstation: %v", err) + } + + // When + services, err := workstation.createServices() + + // Then + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + if len(services) != 0 { + t.Error("Expected no services when docker is disabled") + } + }) + + t.Run("ServiceInitializationError", 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) + } + + // When + services, err := workstation.createServices() + + // Then + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + if len(services) == 0 { + t.Error("Expected services to be created") + } + }) + + t.Run("CreatesDNSService", 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) + } + + // When + services, err := workstation.createServices() + + // Then + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + if len(services) == 0 { + t.Error("Expected services to be created") + } + }) + + t.Run("CreatesGitLivereloadService", 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) + } + + // When + services, err := workstation.createServices() + + // Then + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + if len(services) == 0 { + t.Error("Expected services to be created") + } + }) + + t.Run("CreatesLocalstackService", 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) + } + + // When + services, err := workstation.createServices() + + // Then + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + if len(services) == 0 { + t.Error("Expected services to be created") + } + }) + + t.Run("CreatesRegistryServices", 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) + } + + // When + services, err := workstation.createServices() + + // Then + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + if len(services) == 0 { + t.Error("Expected services to be created") + } + }) + + t.Run("CreatesTalosServices", 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) + } + + // When + services, err := workstation.createServices() + + // Then + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + if len(services) == 0 { + t.Error("Expected services to be created") + } + }) +} + +// ============================================================================= +// Test Helpers +// ============================================================================= + +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) + if err != nil { + t.Fatalf("Failed to create workstation: %v", err) + } + + // When - Up + err = workstation.Up() + + // Then + if err != nil { + t.Errorf("Expected Up to succeed, got error: %v", err) + } + + // When - Down + err = workstation.Down() + + // Then + if err != nil { + t.Errorf("Expected Down to succeed, got error: %v", err) + } + }) + + t.Run("MultipleUpDownCycles", 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) + } + + // When - Multiple cycles + for i := 0; i < 3; i++ { + err = workstation.Up() + if err != nil { + t.Errorf("Expected Up cycle %d to succeed, got error: %v", i+1, err) + } + + err = workstation.Down() + if err != nil { + t.Errorf("Expected Down cycle %d to succeed, got error: %v", i+1, err) + } + } + }) +}