diff --git a/cmd/check_test.go b/cmd/check_test.go index 68438e169..625d90f51 100644 --- a/cmd/check_test.go +++ b/cmd/check_test.go @@ -2,6 +2,7 @@ package cmd import ( "bytes" + "context" "os" "strings" "testing" @@ -13,6 +14,10 @@ func checkContains(str, substr string) bool { } func TestCheckCmd(t *testing.T) { + t.Cleanup(func() { + rootCmd.SetContext(context.Background()) + }) + setup := func(t *testing.T, withConfig bool) (*bytes.Buffer, *bytes.Buffer) { t.Helper() @@ -57,6 +62,9 @@ func TestCheckCmd(t *testing.T) { // Given a directory with proper configuration stdout, stderr := setup(t, true) + // Reset context for fresh test + rootCmd.SetContext(context.Background()) + // When executing the command err := Execute() @@ -79,6 +87,9 @@ func TestCheckCmd(t *testing.T) { // Given a directory with no configuration _, _ = setup(t, false) + // Reset context for fresh test + rootCmd.SetContext(context.Background()) + // When executing the command err := Execute() @@ -96,6 +107,11 @@ func TestCheckCmd(t *testing.T) { } func TestCheckNodeHealthCmd(t *testing.T) { + // Cleanup: reset rootCmd context after all subtests complete + t.Cleanup(func() { + rootCmd.SetContext(context.Background()) + }) + setup := func(t *testing.T, withConfig bool) (*bytes.Buffer, *bytes.Buffer) { t.Helper() @@ -213,6 +229,9 @@ func TestCheckNodeHealthCmd(t *testing.T) { // Given a directory with no configuration _, _ = setup(t, false) + // Reset context for fresh test + rootCmd.SetContext(context.Background()) + // Setup command args rootCmd.SetArgs([]string{"check", "node-health", "--nodes", "10.0.0.1"}) diff --git a/cmd/context.go b/cmd/context.go index da2bde3b1..8539a6f98 100644 --- a/cmd/context.go +++ b/cmd/context.go @@ -48,8 +48,8 @@ var setContextCmd = &cobra.Command{ if err := runtime.NewRuntime(deps). LoadShell(). LoadConfig(). - SetContext(args[0]). WriteResetToken(). + SetContext(args[0]). Do(); err != nil { return fmt.Errorf("Error setting context: %w", err) } diff --git a/cmd/down.go b/cmd/down.go index a38825c1c..c35600ddd 100644 --- a/cmd/down.go +++ b/cmd/down.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/pipelines" + "github.com/windsorcli/cli/pkg/runtime" ) var ( @@ -25,21 +26,27 @@ var downCmd = &cobra.Command{ // Get shared dependency injector from context injector := cmd.Context().Value(injectorKey).(di.Injector) - // First, run the env pipeline in quiet mode to set up environment variables - var envPipeline pipelines.Pipeline - envPipeline, err := pipelines.WithPipeline(injector, cmd.Context(), "envPipeline") - if err != nil { - return fmt.Errorf("failed to set up env pipeline: %w", err) + // First, set up environment variables using runtime + deps := &runtime.Dependencies{ + Injector: injector, } - envCtx := context.WithValue(cmd.Context(), "quiet", true) - envCtx = context.WithValue(envCtx, "decrypt", true) - if err := envPipeline.Execute(envCtx); err != nil { + if err := runtime.NewRuntime(deps). + LoadShell(). + CheckTrustedDirectory(). + LoadConfig(). + LoadSecretsProviders(). + LoadEnvVars(runtime.EnvVarsOptions{ + Decrypt: true, + Verbose: verbose, + }). + ExecutePostEnvHook(verbose). + Do(); err != nil { return fmt.Errorf("failed to set up environment: %w", err) } // Then, run the init pipeline to initialize the environment var initPipeline pipelines.Pipeline - initPipeline, err = pipelines.WithPipeline(injector, cmd.Context(), "initPipeline") + initPipeline, err := pipelines.WithPipeline(injector, cmd.Context(), "initPipeline") if err != nil { return fmt.Errorf("failed to set up init pipeline: %w", err) } diff --git a/cmd/down_test.go b/cmd/down_test.go index d2af55e57..331aa6063 100644 --- a/cmd/down_test.go +++ b/cmd/down_test.go @@ -3,7 +3,6 @@ package cmd import ( "context" "fmt" - "os" "strings" "testing" @@ -34,20 +33,10 @@ type DownMocks struct { func setupDownMocks(t *testing.T, opts ...*SetupOptions) *DownMocks { t.Helper() - // Set up temporary directory and change to it - tmpDir := t.TempDir() - oldDir, _ := os.Getwd() - os.Chdir(tmpDir) - t.Cleanup(func() { os.Chdir(oldDir) }) - - // Get base mocks + // Get base mocks (includes trusted directory setup) baseMocks := setupMocks(t, opts...) - // Register mock env pipeline in injector (needed since down runs env pipeline first) - mockEnvPipeline := pipelines.NewMockBasePipeline() - mockEnvPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockEnvPipeline.ExecuteFunc = func(ctx context.Context) error { return nil } - baseMocks.Injector.Register("envPipeline", mockEnvPipeline) + // Note: env pipeline is no longer used - environment setup is handled by runtime // Register mock init pipeline in injector (needed since down runs init pipeline second) mockInitPipeline := pipelines.NewMockBasePipeline() @@ -109,51 +98,9 @@ func TestDownCmd(t *testing.T) { } }) - t.Run("ErrorSettingUpEnvPipeline", func(t *testing.T) { - // Given a down command with failing env pipeline setup - mocks := setupDownMocks(t) - // Remove env pipeline from injector to cause failure - mocks.Injector.Register("envPipeline", nil) - cmd := createTestDownCmd() - - // When executing the command - ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) - cmd.SetContext(ctx) - err := cmd.Execute() - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to set up environment") { - t.Errorf("Expected error about env pipeline setup, got: %v", err) - } - }) - - t.Run("ErrorExecutingEnvPipeline", func(t *testing.T) { - // Given a down command with failing env pipeline execution - mocks := setupDownMocks(t) - mockEnvPipeline := pipelines.NewMockBasePipeline() - mockEnvPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockEnvPipeline.ExecuteFunc = func(ctx context.Context) error { - return fmt.Errorf("env pipeline execution failed") - } - mocks.Injector.Register("envPipeline", mockEnvPipeline) - cmd := createTestDownCmd() + // Note: ErrorSettingUpEnvironment test removed - runtime is self-healing and creates missing dependencies - // When executing the command - ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) - cmd.SetContext(ctx) - err := cmd.Execute() - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to set up environment") { - t.Errorf("Expected error about environment setup, got: %v", err) - } - }) + // Note: ErrorExecutingEnvPipeline test removed - env pipeline no longer used t.Run("ErrorExecutingInitPipeline", func(t *testing.T) { // Given a down command with failing init pipeline execution @@ -244,40 +191,7 @@ func TestDownCmd(t *testing.T) { } }) - t.Run("EnvPipelineContextFlags", func(t *testing.T) { - // Given a down command with mocks - mocks := setupDownMocks(t) - var envExecutedContext context.Context - mockEnvPipeline := pipelines.NewMockBasePipeline() - mockEnvPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockEnvPipeline.ExecuteFunc = func(ctx context.Context) error { - envExecutedContext = ctx - return nil - } - mocks.Injector.Register("envPipeline", mockEnvPipeline) - cmd := createTestDownCmd() - - // When executing the command - ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) - cmd.SetContext(ctx) - err := cmd.Execute() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And env pipeline should be executed with quiet and decrypt flags - if envExecutedContext == nil { - t.Fatal("Expected context to be passed to env pipeline") - } - if quietValue := envExecutedContext.Value("quiet"); quietValue != true { - t.Errorf("Expected quiet flag to be true, got %v", quietValue) - } - if decryptValue := envExecutedContext.Value("decrypt"); decryptValue != true { - t.Errorf("Expected decrypt flag to be true, got %v", decryptValue) - } - }) + // Note: EnvPipelineContextFlags test removed - env pipeline no longer used t.Run("PipelineOrchestrationOrder", func(t *testing.T) { // Given a down command with mocks @@ -286,13 +200,7 @@ func TestDownCmd(t *testing.T) { // And pipelines that track execution order executionOrder := []string{} - mockEnvPipeline := pipelines.NewMockBasePipeline() - mockEnvPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockEnvPipeline.ExecuteFunc = func(ctx context.Context) error { - executionOrder = append(executionOrder, "env") - return nil - } - mocks.Injector.Register("envPipeline", mockEnvPipeline) + // Note: env pipeline no longer used - environment setup handled by runtime mockInitPipeline := pipelines.NewMockBasePipeline() mockInitPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } @@ -321,7 +229,7 @@ func TestDownCmd(t *testing.T) { t.Errorf("Expected no error, got %v", err) } - expectedOrder := []string{"env", "init", "down"} + expectedOrder := []string{"init", "down"} if len(executionOrder) != len(expectedOrder) { t.Errorf("Expected %d pipelines to execute, got %d", len(expectedOrder), len(executionOrder)) } diff --git a/cmd/env.go b/cmd/env.go index 1b5f9f77a..7e88e5ef3 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -1,13 +1,12 @@ package cmd import ( - "context" "fmt" "os" "github.com/spf13/cobra" "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/pipelines" + "github.com/windsorcli/cli/pkg/runtime" ) var envCmd = &cobra.Command{ @@ -16,12 +15,10 @@ var envCmd = &cobra.Command{ Long: "Output commands to set environment variables for the application.", SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - // Get shared dependency injector from context - injector := cmd.Context().Value(injectorKey).(di.Injector) - // Get flags hook, _ := cmd.Flags().GetBool("hook") decrypt, _ := cmd.Flags().GetBool("decrypt") + verbose, _ := cmd.Flags().GetBool("verbose") // Set NO_CACHE=true unless --hook is specified or NO_CACHE is already set if !hook && os.Getenv("NO_CACHE") == "" { @@ -30,24 +27,45 @@ var envCmd = &cobra.Command{ } } - // Create execution context with flags - ctx := cmd.Context() - if decrypt { - ctx = context.WithValue(ctx, "decrypt", true) + // Create dependencies with injector from command context + deps := &runtime.Dependencies{ + Injector: cmd.Context().Value(injectorKey).(di.Injector), } - if hook { - ctx = context.WithValue(ctx, "hook", true) + + // Create output function for environment variables and aliases + outputFunc := func(output string) { + fmt.Fprint(cmd.OutOrStdout(), output) } - // Set up the env pipeline - pipeline, err := pipelines.WithPipeline(injector, ctx, "envPipeline") - if err != nil { - return fmt.Errorf("failed to set up env pipeline: %w", err) + // Execute the complete workflow + rt := runtime.NewRuntime(deps). + LoadShell(). + CheckTrustedDirectory(). + HandleSessionReset(). + LoadConfig(). + LoadSecretsProviders(). + LoadEnvVars(runtime.EnvVarsOptions{ + Decrypt: decrypt, + Verbose: verbose, + }). + PrintEnvVars(runtime.EnvVarsOptions{ + Verbose: verbose, + Export: hook, + OutputFunc: outputFunc, + }) + + // Only print aliases in hook mode + if hook { + rt = rt.PrintAliases(outputFunc) } - // Execute the pipeline - if err := pipeline.Execute(ctx); err != nil { - return fmt.Errorf("Error executing env pipeline: %w", err) + if err := rt.ExecutePostEnvHook(verbose).Do(); err != nil { + if hook { + // In hook mode, return success even if there are errors + // This prevents shell initialization failures from breaking the environment + return nil + } + return fmt.Errorf("Error executing environment workflow: %w", err) } return nil diff --git a/cmd/env_test.go b/cmd/env_test.go index d657b42d8..b1f629262 100644 --- a/cmd/env_test.go +++ b/cmd/env_test.go @@ -2,10 +2,17 @@ package cmd import ( "bytes" + "context" "testing" ) +// TestEnvCmd tests the Windsor CLI 'env' command for correct environment variable output and error handling across success and decrypt scenarios. +// It ensures proper context management and captures test output for assertion. func TestEnvCmd(t *testing.T) { + t.Cleanup(func() { + rootCmd.SetContext(context.Background()) + }) + setup := func(t *testing.T) (*bytes.Buffer, *bytes.Buffer) { t.Helper() stdout, stderr := captureOutput(t) @@ -20,7 +27,8 @@ func TestEnvCmd(t *testing.T) { // Set up mocks with trusted directory mocks := setupMocks(t) - _ = mocks + ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) + rootCmd.SetContext(ctx) rootCmd.SetArgs([]string{"env"}) @@ -44,7 +52,8 @@ func TestEnvCmd(t *testing.T) { // Set up mocks with trusted directory mocks := setupMocks(t) - _ = mocks + ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) + rootCmd.SetContext(ctx) rootCmd.SetArgs([]string{"env", "--decrypt"}) @@ -65,7 +74,9 @@ func TestEnvCmd(t *testing.T) { t.Run("SuccessWithHook", func(t *testing.T) { // Given proper output capture and mock setup _, stderr := setup(t) - setupMocks(t) + mocks := setupMocks(t) + ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) + rootCmd.SetContext(ctx) rootCmd.SetArgs([]string{"env", "--hook"}) @@ -89,7 +100,8 @@ func TestEnvCmd(t *testing.T) { // Set up mocks with trusted directory mocks := setupMocks(t) - _ = mocks + ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) + rootCmd.SetContext(ctx) rootCmd.SetArgs([]string{"env", "--verbose"}) @@ -110,7 +122,9 @@ func TestEnvCmd(t *testing.T) { t.Run("SuccessWithAllFlags", func(t *testing.T) { // Given proper output capture and mock setup _, stderr := setup(t) - setupMocks(t) + mocks := setupMocks(t) + ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) + rootCmd.SetContext(ctx) rootCmd.SetArgs([]string{"env", "--decrypt", "--hook", "--verbose"}) diff --git a/cmd/exec.go b/cmd/exec.go index 1c4b4f447..26fb9d21e 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/pipelines" + "github.com/windsorcli/cli/pkg/runtime" ) // execCmd represents the exec command @@ -22,19 +23,26 @@ var execCmd = &cobra.Command{ return fmt.Errorf("no command provided") } + verbose, _ := cmd.Flags().GetBool("verbose") + // Get shared dependency injector from context injector := cmd.Context().Value(injectorKey).(di.Injector) - // First, run the env pipeline in quiet mode to set up environment variables - envPipeline, err := pipelines.WithPipeline(injector, cmd.Context(), "envPipeline") - if err != nil { - return fmt.Errorf("failed to set up env pipeline: %w", err) + // First, set up environment variables using runtime + deps := &runtime.Dependencies{ + Injector: injector, } - - // Execute env pipeline in quiet mode (inject environment variables without printing) - envCtx := context.WithValue(cmd.Context(), "quiet", true) - envCtx = context.WithValue(envCtx, "decrypt", true) - if err := envPipeline.Execute(envCtx); err != nil { + if err := runtime.NewRuntime(deps). + LoadShell(). + CheckTrustedDirectory(). + LoadConfig(). + LoadSecretsProviders(). + LoadEnvVars(runtime.EnvVarsOptions{ + Decrypt: true, + Verbose: verbose, + }). + ExecutePostEnvHook(verbose). + Do(); err != nil { return fmt.Errorf("failed to set up environment: %w", err) } diff --git a/cmd/exec_test.go b/cmd/exec_test.go index 2e8010760..7fa024227 100644 --- a/cmd/exec_test.go +++ b/cmd/exec_test.go @@ -3,15 +3,29 @@ package cmd import ( "context" "fmt" - "os" "testing" "github.com/spf13/cobra" "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/pipelines" - "github.com/windsorcli/cli/pkg/shell" ) +// setupExecMocks sets up mocks for exec command tests +func setupExecMocks(t *testing.T, opts ...*SetupOptions) *Mocks { + t.Helper() + + // Get base mocks (includes trusted directory setup) + baseMocks := setupMocks(t, opts...) + + // Register mock exec pipeline in injector + mockExecPipeline := pipelines.NewMockBasePipeline() + mockExecPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } + mockExecPipeline.ExecuteFunc = func(ctx context.Context) error { return nil } + baseMocks.Injector.Register("execPipeline", mockExecPipeline) + + return baseMocks +} + func TestExecCmd(t *testing.T) { createTestCmd := func() *cobra.Command { return &cobra.Command{ @@ -25,72 +39,38 @@ func TestExecCmd(t *testing.T) { } t.Run("Success", func(t *testing.T) { - tmpDir := t.TempDir() - originalDir, _ := os.Getwd() - defer func() { - os.Chdir(originalDir) - }() - os.Chdir(tmpDir) - - injector := di.NewInjector() - - // Register mock shell - mockShell := shell.NewMockShell() - mockShell.CheckTrustedDirectoryFunc = func() error { return nil } - injector.Register("shell", mockShell) - - // Register mock base pipeline - mockBasePipeline := pipelines.NewMockBasePipeline() - injector.Register("basePipeline", mockBasePipeline) - - mockEnvPipeline := pipelines.NewMockBasePipeline() - mockExecPipeline := pipelines.NewMockBasePipeline() - - injector.Register("envPipeline", mockEnvPipeline) - injector.Register("execPipeline", mockExecPipeline) - + // Given proper mock setup + mocks := setupExecMocks(t) cmd := createTestCmd() - ctx := context.WithValue(context.Background(), injectorKey, injector) + ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) cmd.SetContext(ctx) args := []string{"go", "version"} cmd.SetArgs(args) + // When executing the command err := cmd.Execute() + // Then no error should occur if err != nil { t.Errorf("Expected no error, got %v", err) } }) t.Run("NoCommandProvided", func(t *testing.T) { - tmpDir := t.TempDir() - originalDir, _ := os.Getwd() - defer func() { - os.Chdir(originalDir) - }() - os.Chdir(tmpDir) - - injector := di.NewInjector() - - // Register mock shell - mockShell := shell.NewMockShell() - mockShell.CheckTrustedDirectoryFunc = func() error { return nil } - injector.Register("shell", mockShell) - - // Register mock base pipeline - mockBasePipeline := pipelines.NewMockBasePipeline() - injector.Register("basePipeline", mockBasePipeline) - + // Given proper mock setup + mocks := setupExecMocks(t) cmd := createTestCmd() - ctx := context.WithValue(context.Background(), injectorKey, injector) + ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) cmd.SetContext(ctx) args := []string{} cmd.SetArgs(args) + // When executing the command err := cmd.Execute() + // Then an error should be returned if err == nil { t.Error("Expected error, got nil") } @@ -101,90 +81,29 @@ func TestExecCmd(t *testing.T) { } }) - t.Run("EnvPipelineExecutionError", func(t *testing.T) { - tmpDir := t.TempDir() - originalDir, _ := os.Getwd() - defer func() { - os.Chdir(originalDir) - }() - os.Chdir(tmpDir) - - injector := di.NewInjector() - - // Register mock shell - mockShell := shell.NewMockShell() - mockShell.CheckTrustedDirectoryFunc = func() error { return nil } - injector.Register("shell", mockShell) - - // Register mock base pipeline - mockBasePipeline := pipelines.NewMockBasePipeline() - injector.Register("basePipeline", mockBasePipeline) - - mockEnvPipeline := pipelines.NewMockBasePipeline() - mockEnvPipeline.ExecuteFunc = func(context.Context) error { - return fmt.Errorf("env pipeline execution failed") - } - mockExecPipeline := pipelines.NewMockBasePipeline() - - injector.Register("envPipeline", mockEnvPipeline) - injector.Register("execPipeline", mockExecPipeline) - - cmd := createTestCmd() - ctx := context.WithValue(context.Background(), injectorKey, injector) - cmd.SetContext(ctx) - - args := []string{"go", "version"} - cmd.SetArgs(args) - - err := cmd.Execute() - - if err == nil { - t.Error("Expected error, got nil") - } - - expectedError := "failed to set up environment: env pipeline execution failed" - if err.Error() != expectedError { - t.Errorf("Expected error %q, got %q", expectedError, err.Error()) - } - }) + // Note: EnvPipelineExecutionError test removed - env pipeline no longer used t.Run("ExecPipelineExecutionError", func(t *testing.T) { - tmpDir := t.TempDir() - originalDir, _ := os.Getwd() - defer func() { - os.Chdir(originalDir) - }() - os.Chdir(tmpDir) - - injector := di.NewInjector() - - // Register mock shell - mockShell := shell.NewMockShell() - mockShell.CheckTrustedDirectoryFunc = func() error { return nil } - injector.Register("shell", mockShell) - - // Register mock base pipeline - mockBasePipeline := pipelines.NewMockBasePipeline() - injector.Register("basePipeline", mockBasePipeline) - - mockEnvPipeline := pipelines.NewMockBasePipeline() + // Given proper mock setup with failing exec pipeline + mocks := setupExecMocks(t) mockExecPipeline := pipelines.NewMockBasePipeline() + mockExecPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } mockExecPipeline.ExecuteFunc = func(context.Context) error { return fmt.Errorf("exec pipeline execution failed") } - - injector.Register("envPipeline", mockEnvPipeline) - injector.Register("execPipeline", mockExecPipeline) + mocks.Injector.Register("execPipeline", mockExecPipeline) cmd := createTestCmd() - ctx := context.WithValue(context.Background(), injectorKey, injector) + ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) cmd.SetContext(ctx) args := []string{"go", "version"} cmd.SetArgs(args) + // When executing the command err := cmd.Execute() + // Then an error should be returned if err == nil { t.Error("Expected error, got nil") } @@ -196,63 +115,33 @@ func TestExecCmd(t *testing.T) { }) t.Run("ContextValuesPassedCorrectly", func(t *testing.T) { - tmpDir := t.TempDir() - originalDir, _ := os.Getwd() - defer func() { - os.Chdir(originalDir) - }() - os.Chdir(tmpDir) - - injector := di.NewInjector() - - // Register mock shell - mockShell := shell.NewMockShell() - mockShell.CheckTrustedDirectoryFunc = func() error { return nil } - injector.Register("shell", mockShell) - - // Register mock base pipeline - mockBasePipeline := pipelines.NewMockBasePipeline() - injector.Register("basePipeline", mockBasePipeline) - - // Capture context values passed to pipelines - var envContext, execContext context.Context - - mockEnvPipeline := pipelines.NewMockBasePipeline() - mockEnvPipeline.ExecuteFunc = func(ctx context.Context) error { - envContext = ctx - return nil - } + // Given proper mock setup + mocks := setupExecMocks(t) + // Capture context values passed to exec pipeline + var execContext context.Context mockExecPipeline := pipelines.NewMockBasePipeline() + mockExecPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } mockExecPipeline.ExecuteFunc = func(ctx context.Context) error { execContext = ctx return nil } - - injector.Register("envPipeline", mockEnvPipeline) - injector.Register("execPipeline", mockExecPipeline) + mocks.Injector.Register("execPipeline", mockExecPipeline) cmd := createTestCmd() - ctx := context.WithValue(context.Background(), injectorKey, injector) + ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) cmd.SetContext(ctx) args := []string{"test-command", "arg1", "arg2"} cmd.SetArgs(args) + // When executing the command err := cmd.Execute() if err != nil { t.Fatalf("Expected no error, got %v", err) } - // Verify env pipeline context - if envContext.Value("quiet") != true { - t.Error("Expected env pipeline to receive quiet=true") - } - if envContext.Value("decrypt") != true { - t.Error("Expected env pipeline to receive decrypt=true") - } - - // Verify exec pipeline context + // Then exec pipeline should receive correct context values if execContext.Value("command") != "test-command" { t.Errorf("Expected exec pipeline to receive command='test-command', got %v", execContext.Value("command")) } @@ -268,113 +157,73 @@ func TestExecCmd(t *testing.T) { }) t.Run("PipelineCreationAndRegistration", func(t *testing.T) { - tmpDir := t.TempDir() - originalDir, _ := os.Getwd() - defer func() { - os.Chdir(originalDir) - }() - os.Chdir(tmpDir) - - // Create injector with only shell and base pipeline initially - injector := di.NewInjector() - - // Register mock shell and base pipeline (required for exec command) - mockShell := shell.NewMockShell() - mockShell.CheckTrustedDirectoryFunc = func() error { return nil } - injector.Register("shell", mockShell) - - mockBasePipeline := pipelines.NewMockBasePipeline() - injector.Register("basePipeline", mockBasePipeline) - - // Verify pipelines don't exist initially - if injector.Resolve("envPipeline") != nil { - t.Error("Expected env pipeline to not be registered initially") - } - if injector.Resolve("execPipeline") != nil { - t.Error("Expected exec pipeline to not be registered initially") - } + // Given proper mock setup + mocks := setupExecMocks(t) - // Pre-register the pipelines as mocks to simulate successful creation - mockEnvPipeline := pipelines.NewMockBasePipeline() - mockExecPipeline := pipelines.NewMockBasePipeline() - injector.Register("envPipeline", mockEnvPipeline) - injector.Register("execPipeline", mockExecPipeline) + // Verify exec pipeline is registered + execPipeline := mocks.Injector.Resolve("execPipeline") + if execPipeline == nil { + t.Error("Expected exec pipeline to be registered") + } cmd := createTestCmd() - ctx := context.WithValue(context.Background(), injectorKey, injector) + ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) cmd.SetContext(ctx) args := []string{"go", "version"} cmd.SetArgs(args) + // When executing the command err := cmd.Execute() + // Then no error should occur if err != nil { t.Errorf("Expected no error, got %v", err) } - // Verify both pipelines are still registered (reused from injector) - envPipeline := injector.Resolve("envPipeline") - if envPipeline == nil { - t.Error("Expected env pipeline to be registered") - } - - execPipeline := injector.Resolve("execPipeline") + // And exec pipeline should still be registered + execPipeline = mocks.Injector.Resolve("execPipeline") if execPipeline == nil { t.Error("Expected exec pipeline to be registered") } }) t.Run("SingleArgumentCommand", func(t *testing.T) { - tmpDir := t.TempDir() - originalDir, _ := os.Getwd() - defer func() { - os.Chdir(originalDir) - }() - os.Chdir(tmpDir) - - injector := di.NewInjector() - - // Register mock shell - mockShell := shell.NewMockShell() - mockShell.CheckTrustedDirectoryFunc = func() error { return nil } - injector.Register("shell", mockShell) - - // Register mock base pipeline - mockBasePipeline := pipelines.NewMockBasePipeline() - injector.Register("basePipeline", mockBasePipeline) + // Given proper mock setup + mocks := setupExecMocks(t) + // Capture context values passed to exec pipeline var execContext context.Context - mockEnvPipeline := pipelines.NewMockBasePipeline() mockExecPipeline := pipelines.NewMockBasePipeline() + mockExecPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } mockExecPipeline.ExecuteFunc = func(ctx context.Context) error { execContext = ctx return nil } - - injector.Register("envPipeline", mockEnvPipeline) - injector.Register("execPipeline", mockExecPipeline) + mocks.Injector.Register("execPipeline", mockExecPipeline) cmd := createTestCmd() - ctx := context.WithValue(context.Background(), injectorKey, injector) + ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) cmd.SetContext(ctx) args := []string{"single-command"} cmd.SetArgs(args) + // When executing the command err := cmd.Execute() + // Then no error should occur if err != nil { t.Errorf("Expected no error, got %v", err) } - // Verify command is set correctly + // And command should be set correctly command := execContext.Value("command") if command != "single-command" { t.Errorf("Expected command to be 'single-command', got %v", command) } - // Verify args context value is not set for single command + // And args context value should not be set for single command ctxArgs := execContext.Value("args") if ctxArgs != nil { t.Errorf("Expected args to be nil for single command, got %v", ctxArgs) @@ -382,51 +231,32 @@ func TestExecCmd(t *testing.T) { }) t.Run("PipelineReuseWhenAlreadyRegistered", func(t *testing.T) { - tmpDir := t.TempDir() - originalDir, _ := os.Getwd() - defer func() { - os.Chdir(originalDir) - }() - os.Chdir(tmpDir) - - injector := di.NewInjector() - - // Register mock shell - mockShell := shell.NewMockShell() - mockShell.CheckTrustedDirectoryFunc = func() error { return nil } - injector.Register("shell", mockShell) - - // Register mock base pipeline - mockBasePipeline := pipelines.NewMockBasePipeline() - injector.Register("basePipeline", mockBasePipeline) - - // Pre-register pipelines - originalEnvPipeline := pipelines.NewMockBasePipeline() - originalExecPipeline := pipelines.NewMockBasePipeline() + // Given proper mock setup + mocks := setupExecMocks(t) - injector.Register("envPipeline", originalEnvPipeline) - injector.Register("execPipeline", originalExecPipeline) + // Pre-register exec pipeline + originalExecPipeline := pipelines.NewMockBasePipeline() + originalExecPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } + originalExecPipeline.ExecuteFunc = func(ctx context.Context) error { return nil } + mocks.Injector.Register("execPipeline", originalExecPipeline) cmd := createTestCmd() - ctx := context.WithValue(context.Background(), injectorKey, injector) + ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) cmd.SetContext(ctx) args := []string{"go", "version"} cmd.SetArgs(args) + // When executing the command err := cmd.Execute() + // Then no error should occur if err != nil { t.Errorf("Expected no error, got %v", err) } - // Verify same pipeline instances are reused - envPipeline := injector.Resolve("envPipeline") - if envPipeline != originalEnvPipeline { - t.Error("Expected to reuse existing env pipeline") - } - - execPipeline := injector.Resolve("execPipeline") + // And same exec pipeline instance should be reused + execPipeline := mocks.Injector.Resolve("execPipeline") if execPipeline != originalExecPipeline { t.Error("Expected to reuse existing exec pipeline") } diff --git a/cmd/init.go b/cmd/init.go index 917c8931a..7becdcac3 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -10,6 +10,7 @@ import ( "github.com/windsorcli/cli/pkg/config" "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/pipelines" + "github.com/windsorcli/cli/pkg/runtime" ) var ( @@ -52,14 +53,23 @@ var initCmd = &cobra.Command{ initProvider = initPlatform } - ctx = context.WithValue(ctx, "quiet", true) - ctx = context.WithValue(ctx, "decrypt", true) ctx = context.WithValue(ctx, "initPipeline", true) - envPipeline, err := pipelines.WithPipeline(injector, ctx, "envPipeline") - if err != nil { - return fmt.Errorf("failed to set up env pipeline: %w", err) - } - if err := envPipeline.Execute(ctx); err != nil { + + // Set up environment variables using runtime + deps := &runtime.Dependencies{ + Injector: injector, + } + if err := runtime.NewRuntime(deps). + LoadShell(). + CheckTrustedDirectory(). + LoadConfig(). + LoadSecretsProviders(). + LoadEnvVars(runtime.EnvVarsOptions{ + Decrypt: true, + Verbose: verbose, + }). + ExecutePostEnvHook(verbose). + Do(); err != nil { return fmt.Errorf("failed to set up environment: %w", err) } diff --git a/cmd/init_test.go b/cmd/init_test.go index 734e8d285..0b80270f6 100644 --- a/cmd/init_test.go +++ b/cmd/init_test.go @@ -61,12 +61,6 @@ func setupInitTest(t *testing.T, opts ...*SetupOptions) *InitMocks { baseMocks.Shell.AddCurrentDirToTrustedFileFunc = func() error { return nil } baseMocks.Shell.WriteResetTokenFunc = func() (string, error) { return "test-token", nil } - // Register mock env pipeline in injector (needed since init now runs env pipeline first) - mockEnvPipeline := pipelines.NewMockBasePipeline() - mockEnvPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockEnvPipeline.ExecuteFunc = func(ctx context.Context) error { return nil } - baseMocks.Injector.Register("envPipeline", mockEnvPipeline) - // Register mock init pipeline in injector (following exec_test.go pattern) mockInitPipeline := pipelines.NewMockBasePipeline() mockInitPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } @@ -235,6 +229,19 @@ func TestInitCmd(t *testing.T) { mockConfigHandler.SetFunc = func(key string, value interface{}) error { return fmt.Errorf("failed to set %s", key) } + // Set up other methods that the runtime calls + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if len(defaultValue) > 0 { + return defaultValue[0] + } + return "" + } + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + if len(defaultValue) > 0 { + return defaultValue[0] + } + return false + } mocks.Injector.Register("configHandler", mockConfigHandler) // When executing the init command with flags that trigger config setting @@ -1069,6 +1076,19 @@ func TestInitCmd(t *testing.T) { mockConfigHandler.SetFunc = func(key string, value interface{}) error { return fmt.Errorf("failed to set %s", key) } + // Set up other methods that the runtime calls + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if len(defaultValue) > 0 { + return defaultValue[0] + } + return "" + } + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + if len(defaultValue) > 0 { + return defaultValue[0] + } + return false + } mocks.Injector.Register("configHandler", mockConfigHandler) // When executing the init command with flags that trigger config setting diff --git a/cmd/install.go b/cmd/install.go index 856e80496..cf1df05f8 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/pipelines" + "github.com/windsorcli/cli/pkg/runtime" ) var installWaitFlag bool @@ -19,14 +20,21 @@ var installCmd = &cobra.Command{ // Get shared dependency injector from context injector := cmd.Context().Value(injectorKey).(di.Injector) - // First, run the env pipeline in quiet mode to set up environment variables - envPipeline, err := pipelines.WithPipeline(injector, cmd.Context(), "envPipeline") - if err != nil { - return fmt.Errorf("failed to set up env pipeline: %w", err) + // First, set up environment variables using runtime + deps := &runtime.Dependencies{ + Injector: injector, } - envCtx := context.WithValue(cmd.Context(), "quiet", true) - envCtx = context.WithValue(envCtx, "decrypt", true) - if err := envPipeline.Execute(envCtx); err != nil { + if err := runtime.NewRuntime(deps). + LoadShell(). + CheckTrustedDirectory(). + LoadConfig(). + LoadSecretsProviders(). + LoadEnvVars(runtime.EnvVarsOptions{ + Decrypt: true, + Verbose: verbose, + }). + ExecutePostEnvHook(verbose). + Do(); err != nil { return fmt.Errorf("failed to set up environment: %w", err) } diff --git a/cmd/install_test.go b/cmd/install_test.go index ae65169d5..8179c66c9 100644 --- a/cmd/install_test.go +++ b/cmd/install_test.go @@ -26,11 +26,7 @@ func setupInstallTest(t *testing.T, opts ...*SetupOptions) *InstallMocks { // Setup base mocks baseMocks := setupMocks(t, opts...) - // Register mock env pipeline in injector (needed since install runs env pipeline first) - mockEnvPipeline := pipelines.NewMockBasePipeline() - mockEnvPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockEnvPipeline.ExecuteFunc = func(ctx context.Context) error { return nil } - baseMocks.Injector.Register("envPipeline", mockEnvPipeline) + // Note: envPipeline no longer used - install now uses runtime.LoadEnvVars // Register mock install pipeline in injector mockInstallPipeline := pipelines.NewMockBasePipeline() @@ -120,33 +116,7 @@ func TestInstallCmd(t *testing.T) { } }) - t.Run("ReturnsErrorWhenEnvPipelineSetupFails", func(t *testing.T) { - // Given a temporary directory with mocked dependencies - mocks := setupInstallTest(t) - - // Override env pipeline to return error during execution - mockEnvPipeline := pipelines.NewMockBasePipeline() - mockEnvPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockEnvPipeline.ExecuteFunc = func(ctx context.Context) error { - return fmt.Errorf("env pipeline execution failed") - } - mocks.Injector.Register("envPipeline", mockEnvPipeline) - - // When executing the install command - cmd := createTestInstallCmd() - ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) - cmd.SetArgs([]string{}) - cmd.SetContext(ctx) - err := cmd.Execute() - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to set up environment") { - t.Errorf("Expected env pipeline setup error, got %q", err.Error()) - } - }) + // Note: ReturnsErrorWhenEnvPipelineSetupFails test removed - env pipeline no longer used t.Run("ReturnsErrorWhenInstallPipelineSetupFails", func(t *testing.T) { // Given a temporary directory with mocked dependencies diff --git a/cmd/root.go b/cmd/root.go index a594eb7fa..ca3c268b6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,11 +2,6 @@ package cmd import ( "context" - "fmt" - "os" - "path/filepath" - "slices" - "strings" "github.com/spf13/cobra" "github.com/windsorcli/cli/pkg/di" @@ -22,12 +17,19 @@ const injectorKey = contextKey("injector") var shims = NewShims() -// The Execute function is the main entry point for the Windsor CLI application. -// It provides initialization of core dependencies and command execution, -// establishing the dependency injection container context. +// Execute is the main entry point for the Windsor CLI application. +// It initializes core dependencies, establishes the dependency injection container context, +// and executes the root command. If a context with an injector is already set (such as in tests), +// it uses the existing context; otherwise, it creates a new injector and context for normal execution. func Execute() error { + ctx := rootCmd.Context() + if ctx != nil { + if injector, ok := ctx.Value(injectorKey).(di.Injector); ok && injector != nil { + return rootCmd.ExecuteContext(ctx) + } + } injector := di.NewInjector() - ctx := context.WithValue(context.Background(), injectorKey, injector) + ctx = context.WithValue(context.Background(), injectorKey, injector) return rootCmd.ExecuteContext(ctx) } @@ -45,15 +47,12 @@ func init() { } // commandPreflight orchestrates global CLI preflight checks and context initialization for all commands. -// Intended for use as cobra.Command.PersistentPreRunE, it ensures the command context is configured and -// the current directory is authorized for Windsor operations prior to command execution. +// Intended for use as cobra.Command.PersistentPreRunE, it ensures the command context is configured +// prior to command execution. Trust checking is now handled by individual commands through the runtime. func commandPreflight(cmd *cobra.Command, args []string) error { if err := setupGlobalContext(cmd); err != nil { return err } - if err := enforceTrustedDirectory(cmd); err != nil { - return err - } return nil } @@ -70,56 +69,3 @@ func setupGlobalContext(cmd *cobra.Command) error { cmd.SetContext(ctx) return nil } - -// enforceTrustedDirectory checks if the current working directory is trusted for Windsor operations. -// Enforces trust for a defined set of commands, including "env". For "env" with --hook, exits silently to avoid shell integration noise. -// Returns an error if the directory is not trusted. -func enforceTrustedDirectory(cmd *cobra.Command) error { - const notTrustedDirMsg = "not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve" - enforcedCommands := []string{"up", "down", "exec", "install", "env", "build-id"} - cmdName := cmd.Name() - shouldEnforce := slices.Contains(enforcedCommands, cmdName) - - if !shouldEnforce { - return nil - } - - currentDir, err := shims.Getwd() - if err != nil { - return fmt.Errorf("Error getting current directory: %w", err) - } - - homeDir, err := shims.UserHomeDir() - if err != nil { - return fmt.Errorf("Error getting user home directory: %w", err) - } - - trustedDirPath := filepath.Join(homeDir, ".config", "windsor") - trustedFilePath := filepath.Join(trustedDirPath, ".trusted") - - data, err := shims.ReadFile(trustedFilePath) - if err != nil { - if os.IsNotExist(err) { - return fmt.Errorf(notTrustedDirMsg) - } - return fmt.Errorf(notTrustedDirMsg) - } - - iter := strings.SplitSeq(strings.TrimSpace(string(data)), "\n") - - for trustedDir := range iter { - trustedDir = strings.TrimSpace(trustedDir) - if trustedDir != "" && strings.HasPrefix(currentDir, trustedDir) { - return nil - } - } - - if cmdName == "env" { - hook, _ := cmd.Flags().GetBool("hook") - if hook { - shims.Exit(0) - } - } - - return fmt.Errorf(notTrustedDirMsg) -} diff --git a/cmd/root_test.go b/cmd/root_test.go index 74d6abdf1..021b3b66c 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -7,11 +7,9 @@ package cmd import ( "bytes" + "context" "os" "os/exec" - "path/filepath" - "runtime" - "strings" "testing" "github.com/spf13/cobra" @@ -19,6 +17,7 @@ import ( "github.com/windsorcli/cli/pkg/config" "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/env" + "github.com/windsorcli/cli/pkg/kubernetes" "github.com/windsorcli/cli/pkg/secrets" "github.com/windsorcli/cli/pkg/shell" ) @@ -103,6 +102,12 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { return false, nil } mockShell.ResetFunc = func(...bool) {} + mockShell.GetSessionTokenFunc = func() (string, error) { + return "mock-session-token", nil + } + mockShell.WriteResetTokenFunc = func() (string, error) { + return "mock-reset-token", nil + } injector.Register("shell", mockShell) // Create and register mock secrets provider @@ -114,9 +119,7 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { // Create and register mock env printer mockEnvPrinter := env.NewMockEnvPrinter() - mockEnvPrinter.PrintFunc = func() error { - return nil - } + // PrintFunc removed - functionality now in runtime mockEnvPrinter.PostEnvHookFunc = func(directory ...string) error { return nil } @@ -124,18 +127,14 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { // Create and register additional mock env printers mockWindsorEnvPrinter := env.NewMockEnvPrinter() - mockWindsorEnvPrinter.PrintFunc = func() error { - return nil - } + // PrintFunc removed - functionality now in runtime mockWindsorEnvPrinter.PostEnvHookFunc = func(directory ...string) error { return nil } injector.Register("windsorEnvPrinter", mockWindsorEnvPrinter) mockDockerEnvPrinter := env.NewMockEnvPrinter() - mockDockerEnvPrinter.PrintFunc = func() error { - return nil - } + // PrintFunc removed - functionality now in runtime mockDockerEnvPrinter.PostEnvHookFunc = func(directory ...string) error { return nil } @@ -167,6 +166,13 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { } } + // Create and register mock kubernetes manager + mockKubernetesManager := kubernetes.NewMockKubernetesManager(injector) + mockKubernetesManager.InitializeFunc = func() error { + return nil + } + injector.Register("kubernetesManager", mockKubernetesManager) + // Create mock blueprint handler mockBlueprintHandler := blueprintpkg.NewMockBlueprintHandler(injector) mockBlueprintHandler.InstallFunc = func() error { @@ -269,6 +275,11 @@ func TestRootCmd_PersistentPreRunE(t *testing.T) { } func TestCommandPreflight(t *testing.T) { + // Cleanup: reset rootCmd context after all subtests complete + t.Cleanup(func() { + rootCmd.SetContext(context.Background()) + }) + // Set up mocks for all tests setupMocks(t) @@ -278,119 +289,49 @@ func TestCommandPreflight(t *testing.T) { } } - t.Run("SkipsTrustCheckForInitCommand", func(t *testing.T) { + t.Run("SucceedsForInitCommand", func(t *testing.T) { // Given an init command cmd := createMockCmd("init") - // When checking trust + // When running preflight err := commandPreflight(cmd, []string{}) - // Then no error should occur (trust check is skipped) + // Then no error should occur (preflight only sets up global context) if err != nil { t.Errorf("Expected no error for init command, got: %v", err) } }) - t.Run("SkipsTrustCheckForEnvCommandWithHookFlag", func(t *testing.T) { + t.Run("SucceedsForEnvCommandWithHookFlag", func(t *testing.T) { // Given an env command with hook flag cmd := createMockCmd("env") cmd.Flags().Bool("hook", false, "hook flag") cmd.Flags().Set("hook", "true") - // When checking trust + // When running preflight err := commandPreflight(cmd, []string{}) - // Then no error should occur (trust check is skipped for env --hook) + // Then no error should occur (preflight only sets up global context) if err != nil { t.Errorf("Expected no error for env --hook, got: %v", err) } }) - t.Run("ChecksTrustForEnvCommandWithoutHookFlag", func(t *testing.T) { - // Given an env command without hook flag in an untrusted directory - cmd := createMockCmd("env") - cmd.Flags().Bool("hook", false, "hook flag") - - // Override shims to return an untrusted directory - tmpDir := t.TempDir() - origShims := shims - defer func() { shims = origShims }() - - shims = &Shims{ - Exit: func(int) {}, - UserHomeDir: func() (string, error) { return t.TempDir(), nil }, - Getwd: func() (string, error) { return tmpDir, nil }, - ReadFile: func(filename string) ([]byte, error) { - // Return trusted file content that does NOT include tmpDir - return []byte("/test/project\n"), nil - }, - } + t.Run("SetsUpGlobalContext", func(t *testing.T) { + // Given any command + cmd := createMockCmd("test") - // When checking trust + // When running preflight err := commandPreflight(cmd, []string{}) - // Then an error should occur about untrusted directory - if err == nil { - t.Error("Expected error for untrusted directory, got nil") - } - if !strings.Contains(err.Error(), "not in a trusted directory") { - t.Errorf("Expected trust error message, got: %v", err) - } - }) - - t.Run("PassesTrustCheckForTrustedDirectory", func(t *testing.T) { - // Given a command in a trusted directory - cmd := createMockCmd("down") - - // Set up a temporary directory structure with trusted file - tmpDir := t.TempDir() - testDir := filepath.Join(tmpDir, "project") - if err := os.MkdirAll(testDir, 0755); err != nil { - t.Fatalf("Failed to create test directory: %v", err) - } - - // Create trusted file - trustedDir := filepath.Join(tmpDir, ".config", "windsor") - if err := os.MkdirAll(trustedDir, 0755); err != nil { - t.Fatalf("Failed to create trusted directory: %v", err) - } - - trustedFile := filepath.Join(trustedDir, ".trusted") - realTestDir, _ := filepath.EvalSymlinks(testDir) - trustedContent := realTestDir + "\n" - if err := os.WriteFile(trustedFile, []byte(trustedContent), 0644); err != nil { - t.Fatalf("Failed to create trusted file: %v", err) - } - - // Change to test directory - originalDir, err := os.Getwd() + // Then no error should occur (preflight only sets up global context) if err != nil { - t.Fatalf("Failed to get current directory: %v", err) + t.Errorf("Expected no error for preflight, got: %v", err) } - defer os.Chdir(originalDir) - if err := os.Chdir(testDir); err != nil { - t.Fatalf("Failed to change directory: %v", err) - } - - // Mock home directory for cross-platform compatibility - var originalHome string - if runtime.GOOS == "windows" { - originalHome = os.Getenv("USERPROFILE") - defer os.Setenv("USERPROFILE", originalHome) - os.Setenv("USERPROFILE", tmpDir) - } else { - originalHome = os.Getenv("HOME") - defer os.Setenv("HOME", originalHome) - os.Setenv("HOME", tmpDir) - } - - // When checking trust - err = commandPreflight(cmd, []string{}) - - // Then no error should occur - if err != nil { - t.Errorf("Expected no error for trusted directory, got: %v", err) + // And context should be set + if cmd.Context() == nil { + t.Error("Expected command context to be set") } }) diff --git a/cmd/up.go b/cmd/up.go index cc4dc3eae..b4da825f5 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/pipelines" + "github.com/windsorcli/cli/pkg/runtime" ) var ( @@ -23,20 +24,27 @@ var upCmd = &cobra.Command{ // Get shared dependency injector from context injector := cmd.Context().Value(injectorKey).(di.Injector) - // First, run the env pipeline in quiet mode to set up environment variables - envPipeline, err := pipelines.WithPipeline(injector, cmd.Context(), "envPipeline") - if err != nil { - return fmt.Errorf("failed to set up env pipeline: %w", err) + // First, set up environment variables using runtime + deps := &runtime.Dependencies{ + Injector: injector, } - envCtx := context.WithValue(cmd.Context(), "quiet", true) - envCtx = context.WithValue(envCtx, "decrypt", true) - if err := envPipeline.Execute(envCtx); err != nil { + if err := runtime.NewRuntime(deps). + LoadShell(). + CheckTrustedDirectory(). + LoadConfig(). + LoadSecretsProviders(). + LoadEnvVars(runtime.EnvVarsOptions{ + Decrypt: true, + Verbose: verbose, + }). + ExecutePostEnvHook(verbose). + Do(); err != nil { return fmt.Errorf("failed to set up environment: %w", err) } // Then, run the init pipeline to initialize the environment var initPipeline pipelines.Pipeline - initPipeline, err = pipelines.WithPipeline(injector, cmd.Context(), "initPipeline") + initPipeline, err := pipelines.WithPipeline(injector, cmd.Context(), "initPipeline") if err != nil { return fmt.Errorf("failed to set up init pipeline: %w", err) } diff --git a/cmd/up_test.go b/cmd/up_test.go index 5782d1566..44088bd66 100644 --- a/cmd/up_test.go +++ b/cmd/up_test.go @@ -38,11 +38,7 @@ func setupUpTest(t *testing.T, opts ...*SetupOptions) *UpMocks { // Get base mocks baseMocks := setupMocks(t, opts...) - // Register mock env pipeline in injector (needed since up runs env pipeline first) - mockEnvPipeline := pipelines.NewMockBasePipeline() - mockEnvPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockEnvPipeline.ExecuteFunc = func(ctx context.Context) error { return nil } - baseMocks.Injector.Register("envPipeline", mockEnvPipeline) + // Note: envPipeline no longer used - up now uses runtime.LoadEnvVars // Register mock init pipeline in injector (needed since up runs init pipeline second) mockInitPipeline := pipelines.NewMockBasePipeline() @@ -181,33 +177,7 @@ func TestUpCmd(t *testing.T) { } }) - t.Run("EnvPipelineExecutionError", func(t *testing.T) { - // Given a temporary directory with mocked dependencies - mocks := setupUpTest(t) - - // And an env pipeline that fails to execute - mockEnvPipeline := pipelines.NewMockBasePipeline() - mockEnvPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockEnvPipeline.ExecuteFunc = func(ctx context.Context) error { - return fmt.Errorf("env pipeline execution failed") - } - mocks.Injector.Register("envPipeline", mockEnvPipeline) - - // When executing the up command - cmd := createTestUpCmd() - ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) - cmd.SetArgs([]string{}) - cmd.SetContext(ctx) - err := cmd.Execute() - - // Then an error should occur - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to set up environment") { - t.Errorf("Expected env pipeline execution error, got: %v", err) - } - }) + // Note: EnvPipelineExecutionError test removed - env pipeline no longer used t.Run("InitPipelineExecutionError", func(t *testing.T) { // Given a temporary directory with mocked dependencies @@ -364,38 +334,7 @@ func TestUpCmd(t *testing.T) { } }) - t.Run("EnvPipelineContextValues", func(t *testing.T) { - // Given a temporary directory with mocked dependencies - mocks := setupUpTest(t) - - // And an env pipeline that validates its context values - envContextValidated := false - mockEnvPipeline := pipelines.NewMockBasePipeline() - mockEnvPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockEnvPipeline.ExecuteFunc = func(ctx context.Context) error { - // Verify that quiet and decrypt flags are set for env pipeline - if ctx.Value("quiet") == true && ctx.Value("decrypt") == true { - envContextValidated = true - } - return nil - } - mocks.Injector.Register("envPipeline", mockEnvPipeline) - - // When executing the up command - cmd := createTestUpCmd() - ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) - cmd.SetArgs([]string{}) - cmd.SetContext(ctx) - err := cmd.Execute() - - // Then no error should occur and env context should be validated - if err != nil { - t.Errorf("Expected success, got error: %v", err) - } - if !envContextValidated { - t.Error("Expected env pipeline to receive quiet=true and decrypt=true context values") - } - }) + // Note: EnvPipelineContextValues test removed - env pipeline no longer used t.Run("MultipleFlagsCombination", func(t *testing.T) { // Given a temporary directory with mocked dependencies @@ -422,13 +361,7 @@ func TestUpCmd(t *testing.T) { // And pipelines that track execution order executionOrder := []string{} - mockEnvPipeline := pipelines.NewMockBasePipeline() - mockEnvPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockEnvPipeline.ExecuteFunc = func(ctx context.Context) error { - executionOrder = append(executionOrder, "env") - return nil - } - mocks.Injector.Register("envPipeline", mockEnvPipeline) + // Note: env pipeline no longer used - environment setup is handled by runtime mockInitPipeline := pipelines.NewMockBasePipeline() mockInitPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } @@ -465,7 +398,7 @@ func TestUpCmd(t *testing.T) { if err != nil { t.Errorf("Expected success, got error: %v", err) } - expectedOrder := []string{"env", "init", "up", "install"} + expectedOrder := []string{"init", "up", "install"} if len(executionOrder) != len(expectedOrder) { t.Errorf("Expected %d pipeline executions, got %d", len(expectedOrder), len(executionOrder)) } diff --git a/pkg/config/config_handler.go b/pkg/config/config_handler.go index c08b8ab55..a56f71413 100644 --- a/pkg/config/config_handler.go +++ b/pkg/config/config_handler.go @@ -630,25 +630,23 @@ func (c *configHandler) GetContext() string { envContext := c.shims.Getenv("WINDSOR_CONTEXT") if envContext != "" { - c.context = envContext + return envContext } else if c.shell != nil { projectRoot, err := c.shell.GetProjectRoot() if err != nil { - c.context = contextName + return contextName } else { contextFilePath := filepath.Join(projectRoot, windsorDirName, contextFileName) data, err := c.shims.ReadFile(contextFilePath) if err != nil { - c.context = contextName + return contextName } else { - c.context = string(data) + return strings.TrimSpace(string(data)) } } } else { - c.context = contextName + return contextName } - - return c.context } // SetContext sets the current context in the file and updates the cache @@ -673,7 +671,6 @@ func (c *configHandler) SetContext(context string) error { return fmt.Errorf("error setting WINDSOR_CONTEXT environment variable: %w", err) } - c.context = context return nil } diff --git a/pkg/env/aws_env.go b/pkg/env/aws_env.go index e2b5a66a8..a30fab7eb 100644 --- a/pkg/env/aws_env.go +++ b/pkg/env/aws_env.go @@ -81,16 +81,5 @@ func (e *AwsEnvPrinter) GetEnvVars() (map[string]string, error) { return envVars, nil } -// Print prints the environment variables for the AWS environment. -func (e *AwsEnvPrinter) Print() error { - envVars, err := e.GetEnvVars() - if err != nil { - // Return the error if GetEnvVars fails - return fmt.Errorf("error getting environment variables: %w", err) - } - // Call the Print method of the embedded envPrinter struct with the retrieved environment variables - return e.BaseEnvPrinter.Print(envVars) -} - // Ensure AwsEnvPrinter implements the EnvPrinter interface var _ EnvPrinter = (*AwsEnvPrinter)(nil) diff --git a/pkg/env/aws_env_test.go b/pkg/env/aws_env_test.go index f8408c429..8bb2fcf48 100644 --- a/pkg/env/aws_env_test.go +++ b/pkg/env/aws_env_test.go @@ -195,80 +195,3 @@ contexts: } }) } - -// TestAwsEnv_Print tests the Print method of the AwsEnvPrinter -func TestAwsEnv_Print(t *testing.T) { - setup := func() (*AwsEnvPrinter, *Mocks) { - mocks := setupAwsEnvMocks(t) - env := NewAwsEnvPrinter(mocks.Injector) - if err := env.Initialize(); err != nil { - t.Fatalf("Failed to initialize env: %v", err) - } - env.shims = mocks.Shims - return env, mocks - } - - t.Run("Success", func(t *testing.T) { - env, mocks := setup() - - // Mock stat function to make AWS config file exist - env.shims.Stat = func(name string) (os.FileInfo, error) { - if name == filepath.FromSlash("/mock/config/root/.aws/config") { - return nil, nil - } - return nil, os.ErrNotExist - } - - // Mock PrintEnvVarsFunc to capture printed vars - var capturedEnvVars map[string]string - mocks.Shell.PrintEnvVarsFunc = func(envVars map[string]string, export bool) { - capturedEnvVars = envVars - } - - // When calling Print - err := env.Print() - - // Then no error should be returned - if err != nil { - t.Errorf("Print returned an error: %v", err) - } - - // And environment variables should be set correctly - expectedEnvVars := map[string]string{ - "AWS_PROFILE": "default", - "AWS_ENDPOINT_URL": "https://aws.endpoint", - "S3_HOSTNAME": "s3.amazonaws.com", - "MWAA_ENDPOINT": "https://mwaa.endpoint", - "AWS_CONFIG_FILE": "/mock/config/root/.aws/config", - } - if !reflect.DeepEqual(capturedEnvVars, expectedEnvVars) { - t.Errorf("Print set environment variables to %v, want %v", capturedEnvVars, expectedEnvVars) - } - }) - - t.Run("GetConfigError", func(t *testing.T) { - // Given a new AwsEnvPrinter with failing config lookup - mocks := setupAwsEnvMocks(t, &SetupOptions{ - ConfigStr: ` -version: v1alpha1 -contexts: - test-context: {} -`, - }) - env := NewAwsEnvPrinter(mocks.Injector) - if err := env.Initialize(); err != nil { - t.Fatalf("Failed to initialize env: %v", err) - } - - // When calling Print - err := env.Print() - - // Then appropriate error should be returned - if err == nil { - t.Error("Print did not return an error") - } - if !strings.Contains(err.Error(), "context configuration or AWS configuration is missing") { - t.Errorf("Print returned error %v, want error containing 'context configuration or AWS configuration is missing'", err) - } - }) -} diff --git a/pkg/env/azure_env.go b/pkg/env/azure_env.go index 1341f5c1e..8443f1dc5 100644 --- a/pkg/env/azure_env.go +++ b/pkg/env/azure_env.go @@ -66,12 +66,3 @@ func (e *AzureEnvPrinter) GetEnvVars() (map[string]string, error) { return envVars, nil } - -// Print prints the environment variables for the Azure environment. -func (e *AzureEnvPrinter) Print() error { - envVars, err := e.GetEnvVars() - if err != nil { - return fmt.Errorf("error getting environment variables: %w", err) - } - return e.BaseEnvPrinter.Print(envVars) -} diff --git a/pkg/env/azure_env_test.go b/pkg/env/azure_env_test.go index ad55015e4..98a86ee10 100644 --- a/pkg/env/azure_env_test.go +++ b/pkg/env/azure_env_test.go @@ -126,64 +126,3 @@ contexts: } }) } - -func TestAzureEnv_Print(t *testing.T) { - setup := func(t *testing.T, opts ...*SetupOptions) (*AzureEnvPrinter, *Mocks) { - t.Helper() - mocks := setupAzureEnvMocks(t, opts...) - printer := NewAzureEnvPrinter(mocks.Injector) - if err := printer.Initialize(); err != nil { - t.Fatalf("Failed to initialize env: %v", err) - } - printer.shims = mocks.Shims - return printer, mocks - } - - t.Run("Success", func(t *testing.T) { - printer, mocks := setup(t) - configRoot, err := mocks.ConfigHandler.GetConfigRoot() - if err != nil { - t.Fatalf("Failed to get config root: %v", err) - } - var capturedEnvVars map[string]string - mocks.Shell.PrintEnvVarsFunc = func(envVars map[string]string, export bool) { - capturedEnvVars = envVars - } - err = printer.Print() - if err != nil { - t.Errorf("Print returned an error: %v", err) - } - expectedEnvVars := map[string]string{ - "AZURE_CONFIG_DIR": filepath.ToSlash(filepath.Join(configRoot, ".azure")), - "AZURE_CORE_LOGIN_EXPERIENCE_V2": "false", - "ARM_SUBSCRIPTION_ID": "test-subscription", - "ARM_TENANT_ID": "test-tenant", - "ARM_ENVIRONMENT": "test-environment", - } - if !reflect.DeepEqual(capturedEnvVars, expectedEnvVars) { - t.Errorf("Print set environment variables to %v, want %v", capturedEnvVars, expectedEnvVars) - } - }) - - t.Run("GetEnvVarsError", func(t *testing.T) { - mockConfigHandler := &config.MockConfigHandler{} - mockConfigHandler.GetConfigRootFunc = func() (string, error) { - return "", fmt.Errorf("error retrieving configuration root directory") - } - mocks := setupAzureEnvMocks(t, &SetupOptions{ - ConfigHandler: mockConfigHandler, - }) - printer := NewAzureEnvPrinter(mocks.Injector) - if err := printer.Initialize(); err != nil { - t.Fatalf("Failed to initialize env: %v", err) - } - printer.shims = mocks.Shims - err := printer.Print() - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "error getting environment variables") { - t.Errorf("Expected error containing 'error getting environment variables', got %v", err) - } - }) -} diff --git a/pkg/env/docker_env.go b/pkg/env/docker_env.go index 5fbc0844f..be0c0efdf 100644 --- a/pkg/env/docker_env.go +++ b/pkg/env/docker_env.go @@ -129,15 +129,6 @@ func (e *DockerEnvPrinter) GetAlias() (map[string]string, error) { return aliasMap, nil } -// Print retrieves and prints the environment variables for the Docker environment. -func (e *DockerEnvPrinter) Print() error { - envVars, err := e.GetEnvVars() - if err != nil { - return fmt.Errorf("error getting environment variables: %w", err) - } - return e.BaseEnvPrinter.Print(envVars) -} - // ============================================================================= // Private Methods // ============================================================================= diff --git a/pkg/env/docker_env_test.go b/pkg/env/docker_env_test.go index 1e1be16b1..e87f2d4c2 100644 --- a/pkg/env/docker_env_test.go +++ b/pkg/env/docker_env_test.go @@ -5,7 +5,6 @@ import ( "fmt" "os" "path/filepath" - "reflect" "strings" "testing" @@ -640,64 +639,6 @@ func TestDockerEnvPrinter_GetAlias(t *testing.T) { }) } -// TestDockerEnvPrinter_Print tests the Print method of the DockerEnvPrinter -func TestDockerEnvPrinter_Print(t *testing.T) { - // Save original env var and restore after all tests - originalDockerHost := os.Getenv("DOCKER_HOST") - defer os.Setenv("DOCKER_HOST", originalDockerHost) - - t.Run("Success", func(t *testing.T) { - // Given a new DockerEnvPrinter - mocks := setupDockerEnvMocks(t) - printer := NewDockerEnvPrinter(mocks.Injector) - printer.shims = mocks.Shims - printer.Initialize() - - // And PrintEnvVarsFunc is mocked - var capturedEnvVars map[string]string - mocks.Shell.PrintEnvVarsFunc = func(envVars map[string]string, export bool) { - capturedEnvVars = envVars - } - - // When calling Print - err := printer.Print() - - // Then no error should be returned - if err != nil { - t.Errorf("unexpected error: %v", err) - } - - // And environment variables should be set correctly - expectedEnvVars, _ := printer.GetEnvVars() - if !reflect.DeepEqual(capturedEnvVars, expectedEnvVars) { - t.Errorf("capturedEnvVars = %v, want %v", capturedEnvVars, expectedEnvVars) - } - }) - - t.Run("GetEnvVarsError", func(t *testing.T) { - // Given a new DockerEnvPrinter with failing user home directory lookup - os.Unsetenv("DOCKER_HOST") - mocks := setupDockerEnvMocks(t) - - // Override the UserHomeDir shim - mocks.Shims.UserHomeDir = func() (string, error) { - return "", errors.New("mock error") - } - - printer := NewDockerEnvPrinter(mocks.Injector) - printer.shims = mocks.Shims - printer.Initialize() - - // When calling Print - err := printer.Print() - - // Then appropriate error should be returned - if err == nil { - t.Error("expected an error, got nil") - } - }) -} - // TestDockerEnvPrinter_getRegistryURL tests the getRegistryURL method of the DockerEnvPrinter func TestDockerEnvPrinter_getRegistryURL(t *testing.T) { // setup creates a new DockerEnvPrinter with the given configuration diff --git a/pkg/env/env.go b/pkg/env/env.go index 8305f0ef0..bd0a696bf 100644 --- a/pkg/env/env.go +++ b/pkg/env/env.go @@ -21,7 +21,6 @@ import ( // EnvPrinter defines the method for printing environment variables. type EnvPrinter interface { Initialize() error - Print() error GetEnvVars() (map[string]string, error) GetAlias() (map[string]string, error) PostEnvHook(directory ...string) error @@ -76,55 +75,12 @@ func (e *BaseEnvPrinter) Initialize() error { return nil } -// Print outputs the environment variables to the console. -// If a map of key:value strings is provided, it prints those instead. -func (e *BaseEnvPrinter) Print(customVars ...map[string]string) error { - var envVars map[string]string - - if len(customVars) > 0 { - envVars = customVars[0] - } else { - envVars = make(map[string]string) - } - - for key := range envVars { - e.SetManagedEnv(key) - } - - e.shell.PrintEnvVars(envVars, true) - return nil -} - // GetEnvVars is a placeholder for retrieving environment variables. func (e *BaseEnvPrinter) GetEnvVars() (map[string]string, error) { // Placeholder implementation return map[string]string{}, nil } -// PrintAlias retrieves and prints the shell alias. -// If a map of key:value strings is provided, it prints those instead. -func (e *BaseEnvPrinter) PrintAlias(customAlias ...map[string]string) error { - var aliasMap map[string]string - - if len(customAlias) > 0 { - aliasMap = customAlias[0] - } else { - var err error - aliasMap, err = e.GetAlias() - if err != nil { - // Can't test as it just calls a stub - return fmt.Errorf("error getting alias: %w", err) - } - } - - for key := range aliasMap { - e.SetManagedAlias(key) - } - - e.shell.PrintAlias(aliasMap) - return nil -} - // GetAlias is a placeholder for creating an alias for a command. func (e *BaseEnvPrinter) GetAlias() (map[string]string, error) { // Placeholder implementation diff --git a/pkg/env/env_test.go b/pkg/env/env_test.go index 107f7a6d6..c5ed706c0 100644 --- a/pkg/env/env_test.go +++ b/pkg/env/env_test.go @@ -218,132 +218,6 @@ func TestBaseEnvPrinter_GetEnvVars(t *testing.T) { }) } -// TestEnv_Print tests the Print method of the Env struct -func TestEnv_Print(t *testing.T) { - setup := func(t *testing.T) (*BaseEnvPrinter, *Mocks) { - t.Helper() - mocks := setupMocks(t) - printer := NewBaseEnvPrinter(mocks.Injector) - err := printer.Initialize() - if err != nil { - t.Errorf("unexpected error during initialization: %v", err) - } - return printer, mocks - } - - t.Run("Success", func(t *testing.T) { - // Given a new BaseEnvPrinter with test environment variables - printer, mocks := setup(t) - - // And a mock PrintEnvVarsFunc - var capturedEnvVars map[string]string - mocks.Shell.PrintEnvVarsFunc = func(envVars map[string]string, export bool) { - capturedEnvVars = envVars - } - - // And test environment variables - testEnvVars := map[string]string{"TEST_VAR": "test_value"} - - // When calling Print with test environment variables - err := printer.Print(testEnvVars) - - // Then no error should be returned and PrintEnvVarsFunc should be called with correct envVars - if err != nil { - t.Errorf("unexpected error: %v", err) - } - expectedEnvVars := map[string]string{"TEST_VAR": "test_value"} - if !reflect.DeepEqual(capturedEnvVars, expectedEnvVars) { - t.Errorf("capturedEnvVars = %v, want %v", capturedEnvVars, expectedEnvVars) - } - }) - - t.Run("NoCustomVars", func(t *testing.T) { - // Given a new BaseEnvPrinter - printer, mocks := setup(t) - - // And a mock PrintEnvVarsFunc - var capturedEnvVars map[string]string - mocks.Shell.PrintEnvVarsFunc = func(envVars map[string]string, export bool) { - capturedEnvVars = envVars - } - - // When calling Print without custom vars - err := printer.Print() - - // Then no error should be returned and PrintEnvVarsFunc should be called with empty map - if err != nil { - t.Errorf("unexpected error: %v", err) - } - expectedEnvVars := map[string]string{} - if !reflect.DeepEqual(capturedEnvVars, expectedEnvVars) { - t.Errorf("capturedEnvVars = %v, want %v", capturedEnvVars, expectedEnvVars) - } - }) -} - -// TestEnv_PrintAlias tests the PrintAlias method of the Env struct -func TestEnv_PrintAlias(t *testing.T) { - setup := func(t *testing.T) (*BaseEnvPrinter, *Mocks) { - t.Helper() - mocks := setupMocks(t) - printer := NewBaseEnvPrinter(mocks.Injector) - err := printer.Initialize() - if err != nil { - t.Errorf("unexpected error during initialization: %v", err) - } - return printer, mocks - } - - t.Run("SuccessWithCustomAlias", func(t *testing.T) { - // Given a new BaseEnvPrinter with test alias - printer, mocks := setup(t) - - // And a mock PrintAliasFunc - var capturedAlias map[string]string - mocks.Shell.PrintAliasFunc = func(alias map[string]string) { - capturedAlias = alias - } - - // And test alias - testAlias := map[string]string{"alias1": "command1"} - - // When calling PrintAlias with test alias - err := printer.PrintAlias(testAlias) - - // Then no error should be returned and PrintAliasFunc should be called with correct alias - if err != nil { - t.Errorf("unexpected error: %v", err) - } - expectedAlias := map[string]string{"alias1": "command1"} - if !reflect.DeepEqual(capturedAlias, expectedAlias) { - t.Errorf("capturedAlias = %v, want %v", capturedAlias, expectedAlias) - } - }) - - t.Run("SuccessWithoutCustomAlias", func(t *testing.T) { - // Given a new BaseEnvPrinter - printer, mocks := setup(t) - - // And a mock PrintAliasFunc - var capturedAlias map[string]string - mocks.Shell.PrintAliasFunc = func(alias map[string]string) { - capturedAlias = alias - } - - // When calling PrintAlias without custom alias - err := printer.PrintAlias() - - // Then no error should be returned and PrintAliasFunc should be called with empty map - if err != nil { - t.Errorf("unexpected error: %v", err) - } - expectedAlias := map[string]string{} - if !reflect.DeepEqual(capturedAlias, expectedAlias) { - t.Errorf("capturedAlias = %v, want %v", capturedAlias, expectedAlias) - } - }) -} - // TestBaseEnvPrinter_GetManagedEnv tests the GetManagedEnv method of the BaseEnvPrinter struct func TestBaseEnvPrinter_GetManagedEnv(t *testing.T) { setup := func(t *testing.T) (*BaseEnvPrinter, *Mocks) { diff --git a/pkg/env/kube_env.go b/pkg/env/kube_env.go index f2d50e844..07ab6bf75 100644 --- a/pkg/env/kube_env.go +++ b/pkg/env/kube_env.go @@ -136,18 +136,6 @@ func (e *KubeEnvPrinter) GetEnvVars() (map[string]string, error) { return envVars, nil } -// Print prints the environment variables for the Kube environment. -func (e *KubeEnvPrinter) Print() error { - envVars, err := e.GetEnvVars() - if err != nil { - // Return the error if GetEnvVars fails - return fmt.Errorf("error getting environment variables: %w", err) - } - - // Call the Print method of the embedded BaseEnvPrinter struct with the retrieved environment variables - return e.BaseEnvPrinter.Print(envVars) -} - // ============================================================================= // Private Methods // ============================================================================= diff --git a/pkg/env/kube_env_test.go b/pkg/env/kube_env_test.go index fbf9169de..a832ddc07 100644 --- a/pkg/env/kube_env_test.go +++ b/pkg/env/kube_env_test.go @@ -537,54 +537,3 @@ func TestKubeEnvPrinter_GetEnvVars(t *testing.T) { } }) } - -// TestKubeEnvPrinter_Print tests the Print method of the KubeEnvPrinter -func TestKubeEnvPrinter_Print(t *testing.T) { - setup := func(t *testing.T) (*KubeEnvPrinter, *Mocks) { - t.Helper() - mocks := setupKubeEnvMocks(t) - printer := NewKubeEnvPrinter(mocks.Injector) - printer.shims = mocks.Shims - if err := printer.Initialize(); err != nil { - t.Fatalf("Failed to initialize printer: %v", err) - } - return printer, mocks - } - - t.Run("Success", func(t *testing.T) { - // Given a KubeEnvPrinter with valid configuration - printer, _ := setup(t) - - // When printing environment variables - err := printer.Print() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("GetConfigError", func(t *testing.T) { - // Given a mock ConfigHandler that returns an error - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetConfigRootFunc = func() (string, error) { - return "", errors.New("mock config error") - } - - // And a KubeEnvPrinter with the mock ConfigHandler - mocks := setupKubeEnvMocks(t, &SetupOptions{ConfigHandler: mockConfigHandler}) - printer := NewKubeEnvPrinter(mocks.Injector) - printer.shims = mocks.Shims - if err := printer.Initialize(); err != nil { - t.Fatalf("Failed to initialize printer: %v", err) - } - - // When printing environment variables - err := printer.Print() - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - }) -} diff --git a/pkg/env/talos_env.go b/pkg/env/talos_env.go index aee0a4fbb..ce95d18cc 100644 --- a/pkg/env/talos_env.go +++ b/pkg/env/talos_env.go @@ -51,15 +51,5 @@ func (e *TalosEnvPrinter) GetEnvVars() (map[string]string, error) { return envVars, nil } -// Print outputs the environment variables for the Talos environment using the embedded BaseEnvPrinter. -// Returns an error if environment variable retrieval fails or printing fails. -func (e *TalosEnvPrinter) Print() error { - envVars, err := e.GetEnvVars() - if err != nil { - return fmt.Errorf("error getting environment variables: %w", err) - } - return e.BaseEnvPrinter.Print(envVars) -} - // TalosEnvPrinter implements the EnvPrinter interface. var _ EnvPrinter = (*TalosEnvPrinter)(nil) diff --git a/pkg/env/talos_env_test.go b/pkg/env/talos_env_test.go index a6b9c4396..428449d92 100644 --- a/pkg/env/talos_env_test.go +++ b/pkg/env/talos_env_test.go @@ -4,8 +4,6 @@ import ( "errors" "os" "path/filepath" - "reflect" - "strings" "testing" "github.com/windsorcli/cli/pkg/config" @@ -174,144 +172,3 @@ func TestTalosEnv_GetEnvVars(t *testing.T) { } }) } - -// TestTalosEnv_Print tests the Print method of the TalosEnvPrinter -func TestTalosEnv_Print(t *testing.T) { - setup := func(t *testing.T, provider string) (*TalosEnvPrinter, *Mocks) { - t.Helper() - - // Create a mock config handler - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - if key == "provider" { - return provider - } - return "" - } - - mocks := setupMocks(t, &SetupOptions{ - ConfigHandler: mockConfigHandler, - }) - - // Set up GetConfigRoot to return the correct path - mockConfigHandler.GetConfigRootFunc = func() (string, error) { - projectRoot, err := mocks.Shell.GetProjectRoot() - if err != nil { - return "", err - } - return filepath.Join(projectRoot, "contexts", "mock-context"), nil - } - - printer := NewTalosEnvPrinter(mocks.Injector) - if err := printer.Initialize(); err != nil { - t.Fatalf("Failed to initialize env: %v", err) - } - printer.shims = mocks.Shims - - return printer, mocks - } - - t.Run("GenericProviderSuccess", func(t *testing.T) { - // Given a new TalosOmniEnvPrinter with generic provider and existing Talos config - printer, mocks := setup(t, "generic") - - // Get the project root path - projectRoot, err := mocks.Shell.GetProjectRoot() - if err != nil { - t.Fatalf("Failed to get project root: %v", err) - } - expectedPath := filepath.Join(projectRoot, "contexts", "mock-context", ".talos", "config") - - // And Talos config file exists - mocks.Shims.Stat = func(name string) (os.FileInfo, error) { - if name == expectedPath { - return nil, nil - } - return nil, os.ErrNotExist - } - - // And PrintEnvVarsFunc is mocked - var capturedEnvVars map[string]string - mocks.Shell.PrintEnvVarsFunc = func(envVars map[string]string, export bool) { - capturedEnvVars = envVars - } - - // When calling Print - err = printer.Print() - - // Then no error should be returned - if err != nil { - t.Errorf("unexpected error: %v", err) - } - - // And environment variables should be set correctly - expectedEnvVars := map[string]string{ - "TALOSCONFIG": expectedPath, - } - if !reflect.DeepEqual(capturedEnvVars, expectedEnvVars) { - t.Errorf("capturedEnvVars = %v, want %v", capturedEnvVars, expectedEnvVars) - } - }) - - t.Run("OmniProviderSuccess", func(t *testing.T) { - // Given a new TalosOmniEnvPrinter with omni provider and existing config files - printer, mocks := setup(t, "omni") - - // Get the project root path - projectRoot, err := mocks.Shell.GetProjectRoot() - if err != nil { - t.Fatalf("Failed to get project root: %v", err) - } - expectedTalosPath := filepath.Join(projectRoot, "contexts", "mock-context", ".talos", "config") - expectedOmniPath := filepath.Join(projectRoot, "contexts", "mock-context", ".omni", "config") - - // And config files exist - mocks.Shims.Stat = func(name string) (os.FileInfo, error) { - if name == expectedTalosPath || name == expectedOmniPath { - return nil, nil - } - return nil, os.ErrNotExist - } - - // And PrintEnvVarsFunc is mocked - var capturedEnvVars map[string]string - mocks.Shell.PrintEnvVarsFunc = func(envVars map[string]string, export bool) { - capturedEnvVars = envVars - } - - // When calling Print - err = printer.Print() - - // Then no error should be returned - if err != nil { - t.Errorf("unexpected error: %v", err) - } - - // And environment variables should be set correctly - expectedEnvVars := map[string]string{ - "TALOSCONFIG": expectedTalosPath, - "OMNICONFIG": expectedOmniPath, - } - if !reflect.DeepEqual(capturedEnvVars, expectedEnvVars) { - t.Errorf("capturedEnvVars = %v, want %v", capturedEnvVars, expectedEnvVars) - } - }) - - t.Run("GetProjectRootError", func(t *testing.T) { - // Given a new TalosOmniEnvPrinter with failing project root lookup - printer, mocks := setup(t, "generic") - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "", errors.New("mock project root error") - } - - // When calling Print - err := printer.Print() - - // Then appropriate error should be returned - if err == nil { - t.Error("expected error, got nil") - } else if !strings.Contains(err.Error(), "mock project root error") { - t.Errorf("unexpected error message: %v", err) - } - }) -} diff --git a/pkg/env/terraform_env.go b/pkg/env/terraform_env.go index 367e767d9..7ec30c9ff 100644 --- a/pkg/env/terraform_env.go +++ b/pkg/env/terraform_env.go @@ -13,8 +13,8 @@ import ( "sort" "strings" + "github.com/goccy/go-yaml" blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" - "github.com/windsorcli/cli/pkg/blueprint" "github.com/windsorcli/cli/pkg/di" ) @@ -40,7 +40,6 @@ type TerraformArgs struct { // TerraformEnvPrinter is a struct that implements Terraform environment configuration type TerraformEnvPrinter struct { BaseEnvPrinter - blueprintHandler blueprint.BlueprintHandler } // ============================================================================= @@ -58,21 +57,6 @@ func NewTerraformEnvPrinter(injector di.Injector) *TerraformEnvPrinter { // Public Methods // ============================================================================= -// Initialize resolves and assigns dependencies including the blueprint handler from the injector. -func (e *TerraformEnvPrinter) Initialize() error { - if err := e.BaseEnvPrinter.Initialize(); err != nil { - return err - } - - blueprintHandler, ok := e.injector.Resolve("blueprintHandler").(blueprint.BlueprintHandler) - if !ok { - return fmt.Errorf("error resolving blueprintHandler") - } - e.blueprintHandler = blueprintHandler - - return nil -} - // GetEnvVars returns a map of environment variables for Terraform operations. // If not in a Terraform project directory, it unsets managed TF_ variables present in the environment. // Otherwise, it generates Terraform arguments and augments them with dependency variables. @@ -124,15 +108,6 @@ func (e *TerraformEnvPrinter) PostEnvHook(directory ...string) error { return e.generateBackendOverrideTf(directory...) } -// Print outputs the environment variables for the Terraform environment. -func (e *TerraformEnvPrinter) Print() error { - envVars, err := e.GetEnvVars() - if err != nil { - return fmt.Errorf("error getting environment variables: %w", err) - } - return e.BaseEnvPrinter.Print(envVars) -} - // GenerateTerraformArgs constructs Terraform CLI arguments and environment variables for given project and module paths. // Resolves config root, locates tfvars files, generates backend config args, and assembles all CLI/env values needed // for Terraform operations. Returns a TerraformArgs struct or error. @@ -193,16 +168,11 @@ func (e *TerraformEnvPrinter) GenerateTerraformArgs(projectPath, modulePath stri destroyArgs = append(destroyArgs, varFileArgs...) var parallelismArg string - if e.blueprintHandler != nil { - components := e.blueprintHandler.GetTerraformComponents() - for _, component := range components { - if component.Path == projectPath && component.Parallelism != nil { - parallelismArg = fmt.Sprintf(" -parallelism=%d", *component.Parallelism) - applyArgs = append(applyArgs, fmt.Sprintf("-parallelism=%d", *component.Parallelism)) - destroyArgs = append(destroyArgs, fmt.Sprintf("-parallelism=%d", *component.Parallelism)) - break - } - } + component := e.getTerraformComponent(projectPath) + if component != nil && component.Parallelism != nil { + parallelismArg = fmt.Sprintf(" -parallelism=%d", *component.Parallelism) + applyArgs = append(applyArgs, fmt.Sprintf("-parallelism=%d", *component.Parallelism)) + destroyArgs = append(destroyArgs, fmt.Sprintf("-parallelism=%d", *component.Parallelism)) } applyArgs = append(applyArgs, tfPlanPath) @@ -246,28 +216,17 @@ func (e *TerraformEnvPrinter) GenerateTerraformArgs(projectPath, modulePath stri // addDependencyVariables sets dependency outputs as TF_VAR_* environment variables for the specified projectPath. // It locates the current component, resolves dependency order, captures outputs from dependencies, and injects them // into terraformArgs.TerraformVars using the format TF_VAR_. Non-string outputs are stringified. -// If blueprintHandler is nil, or the component has no dependencies, the function is a no-op. Errors are returned for +// If the component has no dependencies, the function is a no-op. Errors are returned for // dependency resolution failures; missing outputs are tolerated. func (e *TerraformEnvPrinter) addDependencyVariables(projectPath string, terraformArgs *TerraformArgs) error { - if e.blueprintHandler == nil { - return nil - } - - components := e.blueprintHandler.GetTerraformComponents() - - var currentComponent *blueprintv1alpha1.TerraformComponent - for _, component := range components { - if component.Path == projectPath { - currentComponent = &component - break - } - } - - if currentComponent == nil { + currentComponent := e.getTerraformComponent(projectPath) + if currentComponent == nil || len(currentComponent.DependsOn) == 0 { return nil } - if len(currentComponent.DependsOn) == 0 { + componentsInterface := e.getTerraformComponents() + components, ok := componentsInterface.([]blueprintv1alpha1.TerraformComponent) + if !ok || len(components) == 0 { return nil } @@ -382,7 +341,11 @@ func (e *TerraformEnvPrinter) resolveTerraformComponentDependencies(components [ // creates backend_override.tf, runs terraform output, and performs cleanup. Returns an empty map for any error to avoid blocking the env pipeline. func (e *TerraformEnvPrinter) captureTerraformOutputs(modulePath string) (map[string]any, error) { var componentPath string - components := e.blueprintHandler.GetTerraformComponents() + componentsInterface := e.getTerraformComponents() + components, ok := componentsInterface.([]blueprintv1alpha1.TerraformComponent) + if !ok { + return make(map[string]any), nil + } for _, component := range components { if component.FullPath == modulePath { componentPath = component.Path @@ -714,5 +677,72 @@ func (e *TerraformEnvPrinter) findRelativeTerraformProjectPath(directory ...stri return "", nil } +// getTerraformComponent finds a Terraform component by path. +// Returns nil if not found. +func (e *TerraformEnvPrinter) getTerraformComponent(projectPath string) *blueprintv1alpha1.TerraformComponent { + componentsInterface := e.getTerraformComponents() + components, ok := componentsInterface.([]blueprintv1alpha1.TerraformComponent) + if !ok { + return nil + } + for _, component := range components { + if component.Path == projectPath { + return &component + } + } + return nil +} + +// getTerraformComponents loads and parses Terraform components from a blueprint.yaml file. +// If projectPath is provided and not empty, it returns a pointer to the matching TerraformComponent or nil if not found. +// If projectPath is not provided, it returns a slice of all TerraformComponent structs from blueprint.yaml. +// For each component, the FullPath field is set to the resolved absolute path for sourced components, or the relative path for local components. +func (e *TerraformEnvPrinter) getTerraformComponents(projectPath ...string) interface{} { + configRoot, err := e.configHandler.GetConfigRoot() + if err != nil { + if len(projectPath) > 0 { + return nil + } + return []blueprintv1alpha1.TerraformComponent{} + } + + blueprintPath := filepath.Join(configRoot, "blueprint.yaml") + data, err := e.shims.ReadFile(blueprintPath) + if err != nil { + if len(projectPath) > 0 { + return nil + } + return []blueprintv1alpha1.TerraformComponent{} + } + + var blueprint blueprintv1alpha1.Blueprint + if err := yaml.Unmarshal(data, &blueprint); err != nil { + if len(projectPath) > 0 { + return nil + } + return []blueprintv1alpha1.TerraformComponent{} + } + + for i := range blueprint.TerraformComponents { + component := &blueprint.TerraformComponents[i] + if component.Source != "" { + component.FullPath = filepath.Join(configRoot, "terraform", component.Path) + } else { + component.FullPath = component.Path + } + } + + if len(projectPath) > 0 { + for i := range blueprint.TerraformComponents { + if blueprint.TerraformComponents[i].Path == projectPath[0] { + return &blueprint.TerraformComponents[i] + } + } + return nil + } + + return blueprint.TerraformComponents +} + // Ensure TerraformEnvPrinter implements the EnvPrinter interface var _ EnvPrinter = (*TerraformEnvPrinter)(nil) diff --git a/pkg/env/terraform_env_test.go b/pkg/env/terraform_env_test.go index 55fbd23ae..81dcdffcb 100644 --- a/pkg/env/terraform_env_test.go +++ b/pkg/env/terraform_env_test.go @@ -444,105 +444,6 @@ func TestTerraformEnv_PostEnvHook(t *testing.T) { }) } -func TestTerraformEnv_Print(t *testing.T) { - setup := func(t *testing.T) (*TerraformEnvPrinter, *Mocks) { - t.Helper() - mocks := setupTerraformEnvMocks(t) - printer := NewTerraformEnvPrinter(mocks.Injector) - printer.shims = mocks.Shims - if err := printer.Initialize(); err != nil { - t.Fatalf("Failed to initialize printer: %v", err) - } - return printer, mocks - } - - t.Run("Success", func(t *testing.T) { - // Given a TerraformEnvPrinter with mock configuration - printer, mocks := setup(t) - - var capturedEnvVars map[string]string - mocks.Shell.PrintEnvVarsFunc = func(envVars map[string]string, export bool) { - capturedEnvVars = envVars - } - - // When Print is called - err := printer.Print() - if err != nil { - t.Errorf("unexpected error: %v", err) - } - - // Then the expected environment variables should be set - expectedOSType := "unix" - if mocks.Shims.Goos() == "windows" { - expectedOSType = "windows" - } - - configRoot, err := mocks.ConfigHandler.GetConfigRoot() - if err != nil { - t.Fatalf("Failed to get config root: %v", err) - } - - expectedEnvVars := map[string]string{ - "TF_DATA_DIR": filepath.Join(configRoot, ".terraform/project/path"), - "TF_CLI_ARGS_init": fmt.Sprintf(`-backend=true -force-copy -upgrade -backend-config="path=%s"`, filepath.Join(configRoot, ".tfstate/project/path/terraform.tfstate")), - "TF_CLI_ARGS_plan": fmt.Sprintf(`-out="%s" -var-file="%s" -var-file="%s"`, - filepath.Join(configRoot, ".terraform/project/path/terraform.tfplan"), - filepath.Join(configRoot, "terraform/project/path.tfvars"), - filepath.Join(configRoot, "terraform/project/path.tfvars.json")), - "TF_CLI_ARGS_apply": fmt.Sprintf(`"%s"`, filepath.Join(configRoot, ".terraform/project/path/terraform.tfplan")), - "TF_CLI_ARGS_refresh": fmt.Sprintf(`-var-file="%s" -var-file="%s"`, - filepath.Join(configRoot, "terraform/project/path.tfvars"), - filepath.Join(configRoot, "terraform/project/path.tfvars.json")), - "TF_CLI_ARGS_import": fmt.Sprintf(`-var-file="%s" -var-file="%s"`, - filepath.Join(configRoot, "terraform/project/path.tfvars"), - filepath.Join(configRoot, "terraform/project/path.tfvars.json")), - "TF_CLI_ARGS_destroy": fmt.Sprintf(`-var-file="%s" -var-file="%s"`, - filepath.Join(configRoot, "terraform/project/path.tfvars"), - filepath.Join(configRoot, "terraform/project/path.tfvars.json")), - "TF_VAR_context_path": configRoot, - "TF_VAR_context_id": "", - "TF_VAR_os_type": expectedOSType, - } - - for k, v := range expectedEnvVars { - expectedEnvVars[k] = filepath.ToSlash(v) - } - for k, v := range capturedEnvVars { - capturedEnvVars[k] = filepath.ToSlash(v) - } - - if !reflect.DeepEqual(capturedEnvVars, expectedEnvVars) { - t.Errorf("capturedEnvVars = %v, want %v", capturedEnvVars, expectedEnvVars) - } - }) - - t.Run("GetConfigError", func(t *testing.T) { - // Given a TerraformEnvPrinter with a failing config handler - configHandler := config.NewMockConfigHandler() - configHandler.GetConfigRootFunc = func() (string, error) { - return "", fmt.Errorf("mock config error") - } - mocks := setupTerraformEnvMocks(t, &SetupOptions{ - ConfigHandler: configHandler, - }) - terraformEnvPrinter := NewTerraformEnvPrinter(mocks.Injector) - terraformEnvPrinter.shims = mocks.Shims - if err := terraformEnvPrinter.Initialize(); err != nil { - t.Fatalf("Failed to initialize printer: %v", err) - } - - // When Print is called - err := terraformEnvPrinter.Print() - - // Then an error should be returned - if err == nil { - t.Error("expected error, got nil") - } else if !strings.Contains(err.Error(), "mock config error") { - t.Errorf("unexpected error message: %v", err) - } - }) -} - func TestTerraformEnv_findRelativeTerraformProjectPath(t *testing.T) { setup := func(t *testing.T) (*TerraformEnvPrinter, *Mocks) { t.Helper() @@ -1383,26 +1284,29 @@ func TestTerraformEnv_DependencyResolution(t *testing.T) { t.Run("ValidDependencyChain", func(t *testing.T) { printer, mocks := setup(t) - // Get the blueprint handler from the injector and configure it - blueprintHandler := mocks.Injector.Resolve("blueprintHandler").(*blueprint.MockBlueprintHandler) - blueprintHandler.GetTerraformComponentsFunc = func() []blueprintv1alpha1.TerraformComponent { - return []blueprintv1alpha1.TerraformComponent{ - { - Path: "vpc", - FullPath: "/project/.windsor/.tf_modules/vpc", - DependsOn: []string{}, - }, - { - Path: "subnets", - FullPath: "/project/.windsor/.tf_modules/subnets", - DependsOn: []string{"vpc"}, - }, - { - Path: "app", - FullPath: "/project/.windsor/.tf_modules/app", - DependsOn: []string{"subnets"}, - }, + // Mock blueprint.yaml content + blueprintYAML := `apiVersion: v1alpha1 +kind: Blueprint +metadata: + name: test-blueprint +terraform: + - path: vpc + fullPath: /project/.windsor/.tf_modules/vpc + dependsOn: [] + - path: subnets + fullPath: /project/.windsor/.tf_modules/subnets + dependsOn: [vpc] + - path: app + fullPath: /project/.windsor/.tf_modules/app + dependsOn: [subnets]` + + // Mock ReadFile to return blueprint.yaml content + originalReadFile := mocks.Shims.ReadFile + mocks.Shims.ReadFile = func(filename string) ([]byte, error) { + if strings.Contains(filename, "blueprint.yaml") { + return []byte(blueprintYAML), nil } + return originalReadFile(filename) } // Mock terraform output for dependencies @@ -1463,26 +1367,29 @@ func TestTerraformEnv_DependencyResolution(t *testing.T) { t.Run("CircularDependencyDetection", func(t *testing.T) { printer, mocks := setup(t) - // Get the blueprint handler from the injector and configure it - blueprintHandler := mocks.Injector.Resolve("blueprintHandler").(*blueprint.MockBlueprintHandler) - blueprintHandler.GetTerraformComponentsFunc = func() []blueprintv1alpha1.TerraformComponent { - return []blueprintv1alpha1.TerraformComponent{ - { - Path: "a", - FullPath: "/project/.windsor/.tf_modules/a", - DependsOn: []string{"b"}, - }, - { - Path: "b", - FullPath: "/project/.windsor/.tf_modules/b", - DependsOn: []string{"c"}, - }, - { - Path: "c", - FullPath: "/project/.windsor/.tf_modules/c", - DependsOn: []string{"a"}, - }, + // Mock blueprint.yaml content with circular dependencies + blueprintYAML := `apiVersion: v1alpha1 +kind: Blueprint +metadata: + name: test-blueprint +terraform: + - path: a + fullPath: /project/.windsor/.tf_modules/a + dependsOn: [b] + - path: b + fullPath: /project/.windsor/.tf_modules/b + dependsOn: [c] + - path: c + fullPath: /project/.windsor/.tf_modules/c + dependsOn: [a]` + + // Mock ReadFile to return blueprint.yaml content + originalReadFile := mocks.Shims.ReadFile + mocks.Shims.ReadFile = func(filename string) ([]byte, error) { + if strings.Contains(filename, "blueprint.yaml") { + return []byte(blueprintYAML), nil } + return originalReadFile(filename) } // Set up the current working directory to match one of the components @@ -1504,16 +1411,23 @@ func TestTerraformEnv_DependencyResolution(t *testing.T) { t.Run("NonExistentDependency", func(t *testing.T) { printer, mocks := setup(t) - // Get the blueprint handler from the injector and configure it - blueprintHandler := mocks.Injector.Resolve("blueprintHandler").(*blueprint.MockBlueprintHandler) - blueprintHandler.GetTerraformComponentsFunc = func() []blueprintv1alpha1.TerraformComponent { - return []blueprintv1alpha1.TerraformComponent{ - { - Path: "app", - FullPath: "/project/.windsor/.tf_modules/app", - DependsOn: []string{"nonexistent"}, - }, + // Mock blueprint.yaml content with non-existent dependency + blueprintYAML := `apiVersion: v1alpha1 +kind: Blueprint +metadata: + name: test-blueprint +terraform: + - path: app + fullPath: /project/.windsor/.tf_modules/app + dependsOn: [nonexistent]` + + // Mock ReadFile to return blueprint.yaml content + originalReadFile := mocks.Shims.ReadFile + mocks.Shims.ReadFile = func(filename string) ([]byte, error) { + if strings.Contains(filename, "blueprint.yaml") { + return []byte(blueprintYAML), nil } + return originalReadFile(filename) } // Set up the current working directory to match the component @@ -1535,21 +1449,26 @@ func TestTerraformEnv_DependencyResolution(t *testing.T) { t.Run("ComponentsWithoutNames", func(t *testing.T) { printer, mocks := setup(t) - // Get the blueprint handler from the injector and configure it - blueprintHandler := mocks.Injector.Resolve("blueprintHandler").(*blueprint.MockBlueprintHandler) - blueprintHandler.GetTerraformComponentsFunc = func() []blueprintv1alpha1.TerraformComponent { - return []blueprintv1alpha1.TerraformComponent{ - { - Path: "vpc/main", - FullPath: "/project/.windsor/.tf_modules/vpc/main", - DependsOn: []string{}, - }, - { - Path: "app/frontend", - FullPath: "/project/.windsor/.tf_modules/app/frontend", - DependsOn: []string{"vpc/main"}, - }, + // Mock blueprint.yaml content with components without names + blueprintYAML := `apiVersion: v1alpha1 +kind: Blueprint +metadata: + name: test-blueprint +terraform: + - path: vpc/main + fullPath: /project/.windsor/.tf_modules/vpc/main + dependsOn: [] + - path: app/frontend + fullPath: /project/.windsor/.tf_modules/app/frontend + dependsOn: [vpc/main]` + + // Mock ReadFile to return blueprint.yaml content + originalReadFile := mocks.Shims.ReadFile + mocks.Shims.ReadFile = func(filename string) ([]byte, error) { + if strings.Contains(filename, "blueprint.yaml") { + return []byte(blueprintYAML), nil } + return originalReadFile(filename) } // Mock terraform output @@ -1708,22 +1627,28 @@ func TestTerraformEnv_GenerateTerraformArgs(t *testing.T) { t.Run("GeneratesCorrectArgsWithParallelism", func(t *testing.T) { mocks := setupTerraformEnvMocks(t) - // Set up blueprint handler with parallelism component - mockBlueprint := blueprint.NewMockBlueprintHandler(mocks.Injector) - parallelism := 5 - mockBlueprint.GetTerraformComponentsFunc = func() []blueprintv1alpha1.TerraformComponent { - return []blueprintv1alpha1.TerraformComponent{ - { - Path: "test/path", - Parallelism: ¶llelism, - }, + // Mock blueprint.yaml content with parallelism + blueprintYAML := `apiVersion: v1alpha1 +kind: Blueprint +metadata: + name: test-blueprint +terraform: + - path: test/path + parallelism: 5` + + // Mock ReadFile to return blueprint.yaml content + originalReadFile := mocks.Shims.ReadFile + mocks.Shims.ReadFile = func(filename string) ([]byte, error) { + if strings.Contains(filename, "blueprint.yaml") { + return []byte(blueprintYAML), nil } + return originalReadFile(filename) } - mocks.Injector.Register("blueprintHandler", mockBlueprint) printer := &TerraformEnvPrinter{ BaseEnvPrinter: *NewBaseEnvPrinter(mocks.Injector), } + printer.shims = mocks.Shims if err := printer.Initialize(); err != nil { t.Fatalf("Failed to initialize printer: %v", err) @@ -1813,22 +1738,19 @@ func TestTerraformEnv_GenerateTerraformArgs(t *testing.T) { } }) - t.Run("HandlesNilBlueprintHandler", func(t *testing.T) { + t.Run("HandlesMissingBlueprintFile", func(t *testing.T) { mocks := setupTerraformEnvMocks(t) printer := &TerraformEnvPrinter{ BaseEnvPrinter: *NewBaseEnvPrinter(mocks.Injector), } - // Initialize with the base dependencies but don't fail on missing blueprint handler + // Initialize with the base dependencies if err := printer.BaseEnvPrinter.Initialize(); err != nil { t.Fatalf("Failed to initialize base printer: %v", err) } - // Set blueprint handler to nil explicitly - printer.blueprintHandler = nil - - // When generating terraform args with nil blueprint handler + // When generating terraform args without blueprint.yaml file args, err := printer.GenerateTerraformArgs("test/path", "test/module") // Then no error should be returned @@ -1836,10 +1758,10 @@ func TestTerraformEnv_GenerateTerraformArgs(t *testing.T) { t.Fatalf("Expected no error, got %v", err) } - // And no parallelism should be applied + // And no parallelism should be applied (since no blueprint.yaml exists) for _, arg := range args.ApplyArgs { if strings.Contains(arg, "parallelism") { - t.Errorf("Apply args should not contain parallelism with nil blueprint handler: %v", args.ApplyArgs) + t.Errorf("Apply args should not contain parallelism without blueprint.yaml: %v", args.ApplyArgs) } } }) @@ -1847,22 +1769,28 @@ func TestTerraformEnv_GenerateTerraformArgs(t *testing.T) { t.Run("CorrectArgumentOrdering", func(t *testing.T) { mocks := setupTerraformEnvMocks(t) - // Set up blueprint handler with parallelism - mockBlueprint := blueprint.NewMockBlueprintHandler(mocks.Injector) - parallelism := 3 - mockBlueprint.GetTerraformComponentsFunc = func() []blueprintv1alpha1.TerraformComponent { - return []blueprintv1alpha1.TerraformComponent{ - { - Path: "test/path", - Parallelism: ¶llelism, - }, + // Mock blueprint.yaml content with parallelism + blueprintYAML := `apiVersion: v1alpha1 +kind: Blueprint +metadata: + name: test-blueprint +terraform: + - path: test/path + parallelism: 3` + + // Mock ReadFile to return blueprint.yaml content + originalReadFile := mocks.Shims.ReadFile + mocks.Shims.ReadFile = func(filename string) ([]byte, error) { + if strings.Contains(filename, "blueprint.yaml") { + return []byte(blueprintYAML), nil } + return originalReadFile(filename) } - mocks.Injector.Register("blueprintHandler", mockBlueprint) printer := &TerraformEnvPrinter{ BaseEnvPrinter: *NewBaseEnvPrinter(mocks.Injector), } + printer.shims = mocks.Shims if err := printer.Initialize(); err != nil { t.Fatalf("Failed to initialize printer: %v", err) diff --git a/pkg/env/windsor_env.go b/pkg/env/windsor_env.go index d4d0c1c9c..50c3b4a1a 100644 --- a/pkg/env/windsor_env.go +++ b/pkg/env/windsor_env.go @@ -183,15 +183,6 @@ func (e *WindsorEnvPrinter) GetEnvVars() (map[string]string, error) { return envVars, nil } -// Print prints the environment variables for the Windsor environment. -func (e *WindsorEnvPrinter) Print() error { - envVars, err := e.GetEnvVars() - if err != nil { - return fmt.Errorf("error getting environment variables: %w", err) - } - return e.BaseEnvPrinter.Print(envVars) -} - // ============================================================================= // Private Methods // ============================================================================= diff --git a/pkg/env/windsor_env_test.go b/pkg/env/windsor_env_test.go index 726c47820..51a491ceb 100644 --- a/pkg/env/windsor_env_test.go +++ b/pkg/env/windsor_env_test.go @@ -804,85 +804,6 @@ func TestWindsorEnv_PostEnvHook(t *testing.T) { }) } -// TestWindsorEnv_Print tests the Print method of the WindsorEnvPrinter -func TestWindsorEnv_Print(t *testing.T) { - setup := func(t *testing.T) (*WindsorEnvPrinter, *Mocks) { - t.Helper() - mocks := setupWindsorEnvMocks(t) - printer := NewWindsorEnvPrinter(mocks.Injector) - if err := printer.Initialize(); err != nil { - t.Fatalf("Failed to initialize env: %v", err) - } - printer.shims = mocks.Shims - return printer, mocks - } - - t.Run("Success", func(t *testing.T) { - printer, mocks := setup(t) - - // Given a WindsorEnvPrinter with project root - projectRoot, err := mocks.Shell.GetProjectRoot() - if err != nil { - t.Fatalf("Failed to get project root: %v", err) - } - - // And a mock PrintEnvVars function - var capturedEnvVars map[string]string - mocks.Shell.PrintEnvVarsFunc = func(envVars map[string]string, export bool) { - capturedEnvVars = envVars - } - - // When Print is called - err = printer.Print() - - // Then no error should be returned - if err != nil { - t.Errorf("unexpected error: %v", err) - } - - // And core Windsor environment variables should be set correctly - if capturedEnvVars["WINDSOR_CONTEXT"] != "mock-context" { - t.Errorf("WINDSOR_CONTEXT = %q, want %q", capturedEnvVars["WINDSOR_CONTEXT"], "mock-context") - } - - if capturedEnvVars["WINDSOR_PROJECT_ROOT"] != projectRoot { - t.Errorf("WINDSOR_PROJECT_ROOT = %q, want %q", capturedEnvVars["WINDSOR_PROJECT_ROOT"], projectRoot) - } - - if capturedEnvVars["WINDSOR_SESSION_TOKEN"] == "" { - t.Errorf("WINDSOR_SESSION_TOKEN is empty") - } - - // And WINDSOR_MANAGED_ENV should include core Windsor variables - managedEnv := capturedEnvVars["WINDSOR_MANAGED_ENV"] - coreVars := []string{"WINDSOR_CONTEXT", "WINDSOR_CONTEXT_ID", "WINDSOR_PROJECT_ROOT", "WINDSOR_SESSION_TOKEN", "WINDSOR_MANAGED_ENV", "WINDSOR_MANAGED_ALIAS"} - for _, v := range coreVars { - if !strings.Contains(managedEnv, v) { - t.Errorf("WINDSOR_MANAGED_ENV should contain %q, got %q", v, managedEnv) - } - } - }) - - t.Run("GetProjectRootError", func(t *testing.T) { - printer, mocks := setup(t) - - // Given a WindsorEnvPrinter with failing project root lookup - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("mock project root error") - } - - // When Print is called - err := printer.Print() - - // Then an error should be returned - if err == nil { - t.Error("expected error, got nil") - } else if !strings.Contains(err.Error(), "mock project root error") { - t.Errorf("unexpected error message: %v", err) - } - }) -} - // TestWindsorEnv_Initialize tests the Initialize method of the WindsorEnvPrinter func TestWindsorEnv_Initialize(t *testing.T) { setup := func(t *testing.T) (*WindsorEnvPrinter, *Mocks) { diff --git a/pkg/kubernetes/mock_kubernetes_manager.go b/pkg/kubernetes/mock_kubernetes_manager.go index 726ea0c8a..45ce2b494 100644 --- a/pkg/kubernetes/mock_kubernetes_manager.go +++ b/pkg/kubernetes/mock_kubernetes_manager.go @@ -46,6 +46,9 @@ func NewMockKubernetesManager(injector di.Injector) *MockKubernetesManager { return &MockKubernetesManager{} } +// Ensure MockKubernetesManager implements KubernetesManager interface +var _ KubernetesManager = (*MockKubernetesManager)(nil) + // ============================================================================= // Public Methods // ============================================================================= diff --git a/pkg/pipelines/env.go b/pkg/pipelines/env.go deleted file mode 100644 index 2dd54fb76..000000000 --- a/pkg/pipelines/env.go +++ /dev/null @@ -1,188 +0,0 @@ -package pipelines - -import ( - "context" - "fmt" - "os" - - "github.com/windsorcli/cli/pkg/blueprint" - "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/env" - "github.com/windsorcli/cli/pkg/secrets" -) - -// The EnvPipeline is a specialized component that manages environment variable printing functionality. -// It provides environment-specific command execution including environment variable collection, -// secrets decryption, and environment injection for the Windsor CLI env command. -// The EnvPipeline handles environment variable management with proper initialization and validation. - -// ============================================================================= -// Types -// ============================================================================= - -// EnvPipeline provides environment variable printing functionality -type EnvPipeline struct { - BasePipeline - blueprintHandler blueprint.BlueprintHandler - envPrinters []env.EnvPrinter - secretsProviders []secrets.SecretsProvider -} - -// ============================================================================= -// Constructor -// ============================================================================= - -// NewEnvPipeline creates a new EnvPipeline instance -func NewEnvPipeline() *EnvPipeline { - return &EnvPipeline{ - BasePipeline: *NewBasePipeline(), - } -} - -// NewDefaultEnvPipeline creates a new EnvPipeline with all default constructors -func NewDefaultEnvPipeline() *EnvPipeline { - return NewEnvPipeline() -} - -// ============================================================================= -// Public Methods -// ============================================================================= - -// Initialize creates and registers all required components for the env pipeline including -// secrets providers and environment printers. It validates dependencies and ensures -// proper initialization of all components required for environment variable management. -func (p *EnvPipeline) Initialize(injector di.Injector, ctx context.Context) error { - if err := p.BasePipeline.Initialize(injector, ctx); err != nil { - return err - } - - kubernetesClient := p.withKubernetesClient() - if kubernetesClient != nil { - p.injector.Register("kubernetesClient", kubernetesClient) - } - - kubernetesManager := p.withKubernetesManager() - if kubernetesManager != nil { - if err := kubernetesManager.Initialize(); err != nil { - return fmt.Errorf("failed to initialize kubernetes manager: %w", err) - } - } - - p.blueprintHandler = p.withBlueprintHandler() - if p.blueprintHandler != nil { - if err := p.blueprintHandler.Initialize(); err != nil { - return fmt.Errorf("failed to initialize blueprint handler: %w", err) - } - _ = p.blueprintHandler.LoadConfig() - } - - secretsProviders, err := p.withSecretsProviders() - if err != nil { - return fmt.Errorf("failed to create secrets providers: %w", err) - } - p.secretsProviders = secretsProviders - - for i, secretsProvider := range p.secretsProviders { - providerKey := fmt.Sprintf("secretsProvider_%d", i) - p.injector.Register(providerKey, secretsProvider) - } - - for _, secretsProvider := range p.secretsProviders { - if err := secretsProvider.Initialize(); err != nil { - return fmt.Errorf("failed to initialize secrets provider: %w", err) - } - } - - envPrinters, err := p.withEnvPrinters() - if err != nil { - return fmt.Errorf("failed to create env printers: %w", err) - } - p.envPrinters = envPrinters - - for _, envPrinter := range p.envPrinters { - if err := envPrinter.Initialize(); err != nil { - return fmt.Errorf("failed to initialize env printer: %w", err) - } - } - - return nil -} - -// Execute runs the environment variable logic by checking directory trust status, -// handling session reset, loading secrets if requested, collecting and injecting -// environment variables into the process, and printing them unless in quiet mode. -func (p *EnvPipeline) Execute(ctx context.Context) error { - isTrusted := p.shell.CheckTrustedDirectory() == nil - hook, _ := ctx.Value("hook").(bool) - quiet, _ := ctx.Value("quiet").(bool) - - if !isTrusted { - p.shell.Reset(quiet) - if !hook { - fmt.Fprintf(os.Stderr, "\033[33mWarning: You are not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve.\033[0m\n") - } - return nil - } - - if err := p.handleSessionReset(); err != nil { - return fmt.Errorf("failed to handle session reset: %w", err) - } - - if decrypt, ok := ctx.Value("decrypt").(bool); ok && decrypt && len(p.secretsProviders) > 0 { - for _, secretsProvider := range p.secretsProviders { - if err := secretsProvider.LoadSecrets(); err != nil { - verbose, _ := ctx.Value("verbose").(bool) - if verbose { - return fmt.Errorf("failed to load secrets: %w", err) - } - return nil - } - } - } - - allEnvVars := make(map[string]string) - for _, envPrinter := range p.envPrinters { - envVars, err := envPrinter.GetEnvVars() - if err != nil { - return fmt.Errorf("error getting environment variables: %w", err) - } - - for key, value := range envVars { - allEnvVars[key] = value - } - } - - for key, value := range allEnvVars { - if err := os.Setenv(key, value); err != nil { - return fmt.Errorf("error setting environment variable %s: %w", key, err) - } - } - - if !quiet { - p.shell.PrintEnvVars(allEnvVars, hook) - - var firstError error - for _, envPrinter := range p.envPrinters { - if err := envPrinter.PostEnvHook(); err != nil && firstError == nil { - firstError = fmt.Errorf("failed to execute post env hook: %w", err) - } - } - - verbose, _ := ctx.Value("verbose").(bool) - if verbose { - return firstError - } - } - - return nil -} - -// ============================================================================= -// Private Methods -// ============================================================================= - -// ============================================================================= -// Interface Compliance -// ============================================================================= - -var _ Pipeline = (*EnvPipeline)(nil) diff --git a/pkg/pipelines/env_test.go b/pkg/pipelines/env_test.go deleted file mode 100644 index b2e268159..000000000 --- a/pkg/pipelines/env_test.go +++ /dev/null @@ -1,805 +0,0 @@ -package pipelines - -import ( - "context" - "fmt" - "strings" - "testing" - - "github.com/windsorcli/cli/pkg/config" - "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/env" - "github.com/windsorcli/cli/pkg/secrets" - "github.com/windsorcli/cli/pkg/shell" -) - -// ============================================================================= -// Test Setup -// ============================================================================= - -type EnvMocks struct { - Injector di.Injector - ConfigHandler config.ConfigHandler - Shell *shell.MockShell - Shims *Shims -} - -func setupEnvMocks(t *testing.T, opts ...*SetupOptions) *EnvMocks { - t.Helper() - - // Get base mocks - baseMocks := setupMocks(t, opts...) - - // Add env-specific shell mock behaviors - baseMocks.Shell.CheckTrustedDirectoryFunc = func() error { return nil } - baseMocks.Shell.CheckResetFlagsFunc = func() (bool, error) { return false, nil } - baseMocks.Shell.GetSessionTokenFunc = func() (string, error) { return "test-token", nil } - baseMocks.Shell.PrintEnvVarsFunc = func(envVars map[string]string, export bool) {} - baseMocks.Shell.ResetFunc = func(args ...bool) {} - - return &EnvMocks{ - Injector: baseMocks.Injector, - ConfigHandler: baseMocks.ConfigHandler, - Shell: baseMocks.Shell, - Shims: baseMocks.Shims, - } -} - -// ============================================================================= -// Test Constructor -// ============================================================================= - -func TestNewEnvPipeline(t *testing.T) { - t.Run("CreatesWithDefaults", func(t *testing.T) { - // Given creating a new env pipeline - pipeline := NewEnvPipeline() - - // Then pipeline should not be nil - if pipeline == nil { - t.Fatal("Expected pipeline to not be nil") - } - }) -} - -func TestNewDefaultEnvPipeline(t *testing.T) { - t.Run("CreatesWithDefaults", func(t *testing.T) { - // Given creating a new default env pipeline - pipeline := NewDefaultEnvPipeline() - - // Then pipeline should not be nil - if pipeline == nil { - t.Fatal("Expected pipeline to not be nil") - } - }) -} - -// ============================================================================= -// Test Public Methods -// ============================================================================= - -func TestEnvPipeline_Initialize(t *testing.T) { - setup := func(t *testing.T, opts ...*SetupOptions) (*EnvPipeline, *EnvMocks) { - t.Helper() - pipeline := NewEnvPipeline() - mocks := setupEnvMocks(t, opts...) - return pipeline, mocks - } - - t.Run("InitializesSuccessfully", func(t *testing.T) { - // Given an env pipeline with mock dependencies - pipeline, mocks := setup(t) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("ReturnsErrorWhenShellInitializeFails", func(t *testing.T) { - // Given an env pipeline with failing shell initialization - mockShell := shell.NewMockShell() - mockShell.InitializeFunc = func() error { - return fmt.Errorf("shell init error") - } - - setupOptions := &SetupOptions{ - ConfigHandler: config.NewMockConfigHandler(), - } - pipeline, mocks := setup(t, setupOptions) - mocks.Injector.Register("shell", mockShell) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "failed to initialize shell: shell init error" { - t.Errorf("Expected shell init error, got: %v", err) - } - }) - - t.Run("ReturnsErrorWhenConfigHandlerInitializeFails", func(t *testing.T) { - // Given an env pipeline with failing config handler initialization - pipeline := NewEnvPipeline() - - // Create injector and register failing config handler directly - injector := di.NewInjector() - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.InitializeFunc = func() error { - return fmt.Errorf("config handler init error") - } - injector.Register("configHandler", mockConfigHandler) - - // Create and register basic shell - mockShell := shell.NewMockShell() - mockShell.InitializeFunc = func() error { return nil } - mockShell.GetProjectRootFunc = func() (string, error) { return t.TempDir(), nil } - injector.Register("shell", mockShell) - - // Register shims - shims := setupShims(t) - injector.Register("shims", shims) - - // When initializing the pipeline - err := pipeline.Initialize(injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "failed to initialize config handler: config handler init error" { - t.Errorf("Expected config handler init error, got: %v", err) - } - }) - - t.Run("ReturnsErrorWhenLoadConfigFails", func(t *testing.T) { - // Given an env pipeline with failing config loading - mockShell := shell.NewMockShell() - mockShell.InitializeFunc = func() error { - return nil - } - mockShell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("project root error") - } - - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.InitializeFunc = func() error { return nil } - mockConfigHandler.LoadConfigFunc = func() error { - return fmt.Errorf("error retrieving project root: project root error") - } - - setupOptions := &SetupOptions{ - ConfigHandler: mockConfigHandler, - } - pipeline, mocks := setup(t, setupOptions) - mocks.Injector.Register("shell", mockShell) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "failed to load context config: error retrieving project root: project root error" { - t.Errorf("Expected load config error, got: %v", err) - } - }) - - t.Run("InitializesWithoutSecretsProviders", func(t *testing.T) { - // Given an env pipeline with no secrets providers configured - pipeline, mocks := setup(t) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("InitializesWithDefaultEnvPrinters", func(t *testing.T) { - // Given an env pipeline with default env printers - pipeline, mocks := setup(t) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("ReturnsErrorWhenWithSecretsProvidersFails", func(t *testing.T) { - // Given an env pipeline with failing secrets providers creation - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetConfigRootFunc = func() (string, error) { - return "", fmt.Errorf("config root error") - } - - setupOptions := &SetupOptions{ - ConfigHandler: mockConfigHandler, - } - pipeline, mocks := setup(t, setupOptions) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if !strings.Contains(fmt.Sprintf("%v", err), "failed to create secrets providers") { - t.Errorf("Expected secrets providers creation error, got: %v", err) - } - }) - - t.Run("ReturnsErrorWhenSecretsProviderInitializeFails", func(t *testing.T) { - // Given an env pipeline with a failing secrets provider - pipeline, mocks := setup(t) - - // Create a mock secrets provider that fails during initialization - mockSecretsProvider := secrets.NewMockSecretsProvider(nil) - mockSecretsProvider.InitializeFunc = func() error { - return fmt.Errorf("secrets provider init error") - } - - // Initialize the base pipeline first to set up dependencies - err := pipeline.BasePipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize base pipeline: %v", err) - } - - // Set the failing secrets provider directly - pipeline.secretsProviders = []secrets.SecretsProvider{mockSecretsProvider} - - // When trying to initialize the secrets provider - for _, secretsProvider := range pipeline.secretsProviders { - if err := secretsProvider.Initialize(); err != nil { - // Then an error should be returned - expectedError := "secrets provider init error" - if err.Error() != expectedError { - t.Errorf("Expected '%s', got: %v", expectedError, err) - } - return - } - } - - t.Fatal("Expected error, got nil") - }) - - t.Run("ReturnsErrorWhenWithEnvPrintersFails", func(t *testing.T) { - // Given an env pipeline with failing env printers creation - pipeline, mocks := setup(t) - - // Initialize the base pipeline first - err := pipeline.BasePipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize base pipeline: %v", err) - } - - // Set configHandler to nil to cause withEnvPrinters to fail - pipeline.configHandler = nil - - // When calling withEnvPrinters - envPrinters, err := pipeline.withEnvPrinters() - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "config handler not initialized" { - t.Errorf("Expected 'config handler not initialized', got: %v", err) - } - if envPrinters != nil { - t.Error("Expected nil env printers") - } - }) - - t.Run("ReturnsErrorWhenEnvPrinterInitializeFails", func(t *testing.T) { - // Given an env pipeline with failing env printer initialization - pipeline, mocks := setup(t) - - // Create a mock env printer that fails during initialization - mockEnvPrinter := env.NewMockEnvPrinter() - mockEnvPrinter.InitializeFunc = func() error { - return fmt.Errorf("env printer init error") - } - - // Initialize the base pipeline first - err := pipeline.BasePipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize base pipeline: %v", err) - } - - // Set the env printer directly to trigger the error - pipeline.envPrinters = []env.EnvPrinter{mockEnvPrinter} - - // When calling the env printer initialization - for _, envPrinter := range pipeline.envPrinters { - if err := envPrinter.Initialize(); err != nil { - // Then an error should be returned - expectedError := "env printer init error" - if err.Error() != expectedError { - t.Errorf("Expected '%s', got: %v", expectedError, err) - } - return - } - } - - t.Fatal("Expected error, got nil") - }) -} - -// ============================================================================= -// Test Public Methods - Execute -// ============================================================================= - -func TestEnvPipeline_Execute(t *testing.T) { - setup := func(t *testing.T) (*EnvPipeline, *EnvMocks) { - t.Helper() - - pipeline := NewEnvPipeline() - mocks := setupEnvMocks(t) - - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - return pipeline, mocks - } - - t.Run("ExecutesSuccessfullyInTrustedDirectory", func(t *testing.T) { - // Given an env pipeline in a trusted directory - pipeline, mocks := setup(t) - - mocks.Shell.CheckTrustedDirectoryFunc = func() error { - return nil - } - - // When executing the pipeline - err := pipeline.Execute(context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("ResetsShellInUntrustedDirectory", func(t *testing.T) { - // Given an env pipeline in an untrusted directory - pipeline, mocks := setup(t) - - mocks.Shell.CheckTrustedDirectoryFunc = func() error { - return fmt.Errorf("untrusted directory") - } - - resetCalled := false - mocks.Shell.ResetFunc = func(args ...bool) { - resetCalled = true - } - - // When executing the pipeline - err := pipeline.Execute(context.Background()) - - // Then no error should be returned and reset should be called - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if !resetCalled { - t.Error("Expected shell reset to be called") - } - }) - - t.Run("LoadsSecretsWhenDecryptIsTrue", func(t *testing.T) { - // Given an env pipeline with decrypt enabled - pipeline, mocks := setup(t) - - mocks.Shell.CheckTrustedDirectoryFunc = func() error { - return nil - } - - mockSecretsProvider := secrets.NewMockSecretsProvider(nil) - loadSecretsCalled := false - mockSecretsProvider.LoadSecretsFunc = func() error { - loadSecretsCalled = true - return nil - } - pipeline.secretsProviders = []secrets.SecretsProvider{mockSecretsProvider} - - ctx := context.WithValue(context.Background(), "decrypt", true) - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then no error should be returned and secrets should be loaded - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if !loadSecretsCalled { - t.Error("Expected secrets to be loaded") - } - }) - - t.Run("HandlesSecretsLoadingErrorInVerboseMode", func(t *testing.T) { - // Given an env pipeline with failing secrets loading in verbose mode - pipeline, mocks := setup(t) - - mocks.Shell.CheckTrustedDirectoryFunc = func() error { - return nil - } - - mockSecretsProvider := secrets.NewMockSecretsProvider(nil) - mockSecretsProvider.LoadSecretsFunc = func() error { - return fmt.Errorf("secrets load error") - } - pipeline.secretsProviders = []secrets.SecretsProvider{mockSecretsProvider} - - ctx := context.WithValue(context.Background(), "decrypt", true) - ctx = context.WithValue(ctx, "verbose", true) - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "failed to load secrets: secrets load error" { - t.Errorf("Expected secrets load error, got: %v", err) - } - }) - - t.Run("IgnoresSecretsLoadingErrorInNonVerboseMode", func(t *testing.T) { - // Given an env pipeline with failing secrets loading in non-verbose mode - pipeline, mocks := setup(t) - - mocks.Shell.CheckTrustedDirectoryFunc = func() error { - return nil - } - - mockSecretsProvider := secrets.NewMockSecretsProvider(nil) - mockSecretsProvider.LoadSecretsFunc = func() error { - return fmt.Errorf("secrets load error") - } - pipeline.secretsProviders = []secrets.SecretsProvider{mockSecretsProvider} - - ctx := context.WithValue(context.Background(), "decrypt", true) - ctx = context.WithValue(ctx, "verbose", false) - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("ReturnsErrorWhenGetEnvVarsFails", func(t *testing.T) { - // Given an env pipeline with failing env vars collection - pipeline, mocks := setup(t) - - mocks.Shell.CheckTrustedDirectoryFunc = func() error { - return nil - } - - mockEnvPrinter := env.NewMockEnvPrinter() - mockEnvPrinter.GetEnvVarsFunc = func() (map[string]string, error) { - return nil, fmt.Errorf("env vars error") - } - pipeline.envPrinters = []env.EnvPrinter{mockEnvPrinter} - - // When executing the pipeline - err := pipeline.Execute(context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "error getting environment variables: env vars error" { - t.Errorf("Expected env vars error, got: %v", err) - } - }) - - t.Run("PrintsEnvVarsWhenNotQuiet", func(t *testing.T) { - // Given an env pipeline with quiet mode disabled - pipeline, mocks := setup(t) - - mocks.Shell.CheckTrustedDirectoryFunc = func() error { - return nil - } - - printCalled := false - mocks.Shell.PrintEnvVarsFunc = func(envVars map[string]string, export bool) { - printCalled = true - } - - mockEnvPrinter := env.NewMockEnvPrinter() - postEnvHookCalled := false - mockEnvPrinter.PostEnvHookFunc = func(directory ...string) error { - postEnvHookCalled = true - return nil - } - mockEnvPrinter.GetEnvVarsFunc = func() (map[string]string, error) { - return map[string]string{}, nil - } - pipeline.envPrinters = []env.EnvPrinter{mockEnvPrinter} - - ctx := context.WithValue(context.Background(), "quiet", false) - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then no error should be returned and print methods should be called - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if !printCalled { - t.Error("Expected print to be called") - } - if !postEnvHookCalled { - t.Error("Expected post env hook to be called") - } - }) - - t.Run("SkipsPrintingWhenQuiet", func(t *testing.T) { - // Given an env pipeline with quiet mode enabled - pipeline, mocks := setup(t) - - mocks.Shell.CheckTrustedDirectoryFunc = func() error { - return nil - } - - printCalled := false - mocks.Shell.PrintEnvVarsFunc = func(envVars map[string]string, export bool) { - printCalled = true - } - - mockEnvPrinter := env.NewMockEnvPrinter() - mockEnvPrinter.GetEnvVarsFunc = func() (map[string]string, error) { - return map[string]string{}, nil - } - pipeline.envPrinters = []env.EnvPrinter{mockEnvPrinter} - - ctx := context.WithValue(context.Background(), "quiet", true) - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then no error should be returned and print should not be called - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if printCalled { - t.Error("Expected print to not be called") - } - }) - - t.Run("ReturnsErrorWhenPostEnvHookFailsInVerboseMode", func(t *testing.T) { - // Given an env pipeline with failing post env hook in verbose mode - pipeline, mocks := setup(t) - - mocks.Shell.CheckTrustedDirectoryFunc = func() error { - return nil - } - - mocks.Shell.PrintEnvVarsFunc = func(envVars map[string]string, export bool) {} - - mockEnvPrinter := env.NewMockEnvPrinter() - mockEnvPrinter.PostEnvHookFunc = func(directory ...string) error { - return fmt.Errorf("post env hook error") - } - mockEnvPrinter.GetEnvVarsFunc = func() (map[string]string, error) { - return map[string]string{}, nil - } - pipeline.envPrinters = []env.EnvPrinter{mockEnvPrinter} - - ctx := context.WithValue(context.Background(), "quiet", false) - ctx = context.WithValue(ctx, "verbose", true) - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "failed to execute post env hook: post env hook error" { - t.Errorf("Expected post env hook error, got: %v", err) - } - }) - - t.Run("IgnoresPostEnvHookErrorInNonVerboseMode", func(t *testing.T) { - // Given an env pipeline with failing post env hook in non-verbose mode - pipeline, mocks := setup(t) - - mocks.Shell.CheckTrustedDirectoryFunc = func() error { - return nil - } - - mocks.Shell.PrintEnvVarsFunc = func(envVars map[string]string, export bool) {} - - mockEnvPrinter := env.NewMockEnvPrinter() - mockEnvPrinter.PostEnvHookFunc = func(directory ...string) error { - return fmt.Errorf("post env hook error") - } - mockEnvPrinter.GetEnvVarsFunc = func() (map[string]string, error) { - return map[string]string{}, nil - } - pipeline.envPrinters = []env.EnvPrinter{mockEnvPrinter} - - ctx := context.WithValue(context.Background(), "quiet", false) - ctx = context.WithValue(ctx, "verbose", false) - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("HandlesHookContextInUntrustedDirectory", func(t *testing.T) { - // Given an env pipeline in an untrusted directory with hook context - pipeline, mocks := setup(t) - - mocks.Shell.CheckTrustedDirectoryFunc = func() error { - return fmt.Errorf("untrusted directory") - } - - resetCalled := false - mocks.Shell.ResetFunc = func(args ...bool) { - resetCalled = true - } - - ctx := context.WithValue(context.Background(), "hook", true) - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then no error should be returned and reset should be called - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if !resetCalled { - t.Error("Expected shell reset to be called") - } - }) - - t.Run("HandlesSessionResetError", func(t *testing.T) { - // Given an env pipeline with failing session reset - pipeline, mocks := setup(t) - - mocks.Shell.CheckTrustedDirectoryFunc = func() error { - return nil - } - - mocks.Shell.CheckResetFlagsFunc = func() (bool, error) { - return false, fmt.Errorf("session reset error") - } - - // When executing the pipeline - err := pipeline.Execute(context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to handle session reset") { - t.Errorf("Expected session reset error, got: %v", err) - } - }) - - t.Run("SkipsSecretsLoadingWhenDecryptFalse", func(t *testing.T) { - // Given an env pipeline with decrypt disabled - pipeline, mocks := setup(t) - - mocks.Shell.CheckTrustedDirectoryFunc = func() error { - return nil - } - - mockSecretsProvider := secrets.NewMockSecretsProvider(nil) - loadSecretsCalled := false - mockSecretsProvider.LoadSecretsFunc = func() error { - loadSecretsCalled = true - return nil - } - pipeline.secretsProviders = []secrets.SecretsProvider{mockSecretsProvider} - - ctx := context.WithValue(context.Background(), "decrypt", false) - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then no error should be returned and secrets should not be loaded - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if loadSecretsCalled { - t.Error("Expected secrets to not be loaded") - } - }) - - t.Run("SkipsSecretsLoadingWhenNoSecretsProviders", func(t *testing.T) { - // Given an env pipeline with no secrets providers - pipeline, mocks := setup(t) - - mocks.Shell.CheckTrustedDirectoryFunc = func() error { - return nil - } - - pipeline.secretsProviders = []secrets.SecretsProvider{} - - ctx := context.WithValue(context.Background(), "decrypt", true) - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("CollectsEnvVarsFromMultipleEnvPrinters", func(t *testing.T) { - // Given an env pipeline with multiple env printers - pipeline, mocks := setup(t) - - mocks.Shell.CheckTrustedDirectoryFunc = func() error { - return nil - } - - mockEnvPrinter1 := env.NewMockEnvPrinter() - mockEnvPrinter1.GetEnvVarsFunc = func() (map[string]string, error) { - return map[string]string{"VAR1": "value1"}, nil - } - mockEnvPrinter1.PostEnvHookFunc = func(directory ...string) error { - return nil - } - - mockEnvPrinter2 := env.NewMockEnvPrinter() - mockEnvPrinter2.GetEnvVarsFunc = func() (map[string]string, error) { - return map[string]string{"VAR2": "value2"}, nil - } - mockEnvPrinter2.PostEnvHookFunc = func(directory ...string) error { - return nil - } - - pipeline.envPrinters = []env.EnvPrinter{mockEnvPrinter1, mockEnvPrinter2} - - var capturedEnvVars map[string]string - mocks.Shell.PrintEnvVarsFunc = func(envVars map[string]string, export bool) { - capturedEnvVars = envVars - } - - // When executing the pipeline - err := pipeline.Execute(context.Background()) - - // Then no error should be returned and both variables should be collected - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if capturedEnvVars["VAR1"] != "value1" { - t.Errorf("Expected VAR1=value1, got %s", capturedEnvVars["VAR1"]) - } - if capturedEnvVars["VAR2"] != "value2" { - t.Errorf("Expected VAR2=value2, got %s", capturedEnvVars["VAR2"]) - } - }) -} - -// ============================================================================= -// Test Private Methods -// ============================================================================= - -// NOTE: collectAndSetEnvVars functionality is now integrated into Execute method diff --git a/pkg/pipelines/pipeline.go b/pkg/pipelines/pipeline.go index c17bedb45..217e87bc3 100644 --- a/pkg/pipelines/pipeline.go +++ b/pkg/pipelines/pipeline.go @@ -51,7 +51,6 @@ type PipelineConstructor func() Pipeline // pipelineConstructors maps pipeline names to their constructor functions var pipelineConstructors = map[string]PipelineConstructor{ - "envPipeline": func() Pipeline { return NewEnvPipeline() }, "initPipeline": func() Pipeline { return NewInitPipeline() }, "execPipeline": func() Pipeline { return NewExecPipeline() }, "checkPipeline": func() Pipeline { return NewCheckPipeline() }, diff --git a/pkg/pipelines/pipeline_test.go b/pkg/pipelines/pipeline_test.go index 190aa8262..e4ffc75a3 100644 --- a/pkg/pipelines/pipeline_test.go +++ b/pkg/pipelines/pipeline_test.go @@ -524,7 +524,6 @@ func TestWithPipeline(t *testing.T) { name string pipelineType string }{ - {"EnvPipeline", "envPipeline"}, {"InitPipeline", "initPipeline"}, {"ExecPipeline", "execPipeline"}, {"CheckPipeline", "checkPipeline"}, @@ -582,7 +581,7 @@ func TestWithPipeline(t *testing.T) { mocks := setupMocks(t) // When creating a pipeline with WithPipeline - pipeline, err := WithPipeline(mocks.Injector, context.Background(), "envPipeline") + pipeline, err := WithPipeline(mocks.Injector, context.Background(), "basePipeline") // Then no error should be returned if err != nil { @@ -595,7 +594,7 @@ func TestWithPipeline(t *testing.T) { } // And the pipeline should be registered in the injector - registered := mocks.Injector.Resolve("envPipeline") + registered := mocks.Injector.Resolve("basePipeline") if registered == nil { t.Error("Expected pipeline to be registered in injector") } @@ -605,11 +604,11 @@ func TestWithPipeline(t *testing.T) { // Given an injector with existing pipeline injector := di.NewInjector() existingPipeline := NewMockBasePipeline() - injector.Register("envPipeline", existingPipeline) + injector.Register("initPipeline", existingPipeline) ctx := context.Background() // When creating a pipeline with WithPipeline - pipeline, err := WithPipeline(injector, ctx, "envPipeline") + pipeline, err := WithPipeline(injector, ctx, "initPipeline") // Then no error should be returned if err != nil { @@ -626,11 +625,11 @@ func TestWithPipeline(t *testing.T) { // Given an injector with non-pipeline object injector := di.NewInjector() nonPipeline := "not a pipeline" - injector.Register("envPipeline", nonPipeline) + injector.Register("initPipeline", nonPipeline) ctx := context.Background() // When creating a pipeline with WithPipeline - pipeline, err := WithPipeline(injector, ctx, "envPipeline") + pipeline, err := WithPipeline(injector, ctx, "initPipeline") // Then no error should be returned (it creates a new pipeline) if err != nil { @@ -649,7 +648,7 @@ func TestWithPipeline(t *testing.T) { ctx := context.WithValue(context.Background(), "testKey", "testValue") // When creating a pipeline with WithPipeline - pipeline, err := WithPipeline(injector, ctx, "envPipeline") + pipeline, err := WithPipeline(injector, ctx, "initPipeline") // Then no error should be returned if err != nil { @@ -674,7 +673,7 @@ func TestWithPipeline(t *testing.T) { }() // This should panic due to nil pointer dereference - WithPipeline(nil, ctx, "envPipeline") + WithPipeline(nil, ctx, "initPipeline") // If we reach here, the test should fail t.Error("Expected panic for nil injector, but function returned normally") @@ -692,7 +691,7 @@ func TestWithPipeline(t *testing.T) { }() // This should panic due to nil pointer dereference during initialization - WithPipeline(injector, nil, "envPipeline") + WithPipeline(injector, nil, "initPipeline") // If we reach here, the test should fail t.Error("Expected panic for nil context, but function returned normally") @@ -722,7 +721,6 @@ func TestWithPipeline(t *testing.T) { t.Run("AllSupportedTypes", func(t *testing.T) { supportedTypes := []string{ - "envPipeline", "initPipeline", "execPipeline", "checkPipeline", @@ -800,7 +798,7 @@ func TestWithPipeline(t *testing.T) { ctx := context.Background() // When creating a pipeline with WithPipeline - pipeline, err := WithPipeline(injector, ctx, "envPipeline") + pipeline, err := WithPipeline(injector, ctx, "initPipeline") // Then no error should be returned if err != nil { @@ -808,7 +806,7 @@ func TestWithPipeline(t *testing.T) { } // And the pipeline should be registered in the injector - registered := injector.Resolve("envPipeline") + registered := injector.Resolve("initPipeline") if registered == nil { t.Error("Expected pipeline to be registered in injector") } @@ -823,8 +821,8 @@ func TestWithPipeline(t *testing.T) { ctx := context.Background() // When creating multiple pipelines of the same type - pipeline1, err1 := WithPipeline(injector, ctx, "envPipeline") - pipeline2, err2 := WithPipeline(injector, ctx, "envPipeline") + pipeline1, err1 := WithPipeline(injector, ctx, "initPipeline") + pipeline2, err2 := WithPipeline(injector, ctx, "initPipeline") // Then both calls should succeed if err1 != nil { diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index 95a94e2bd..f37063ece 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -2,6 +2,7 @@ package runtime import ( "fmt" + "maps" "os" "github.com/windsorcli/cli/pkg/artifact" @@ -69,11 +70,21 @@ type Dependencies struct { } } +// EnvVarsOptions contains options for environment variable operations. +type EnvVarsOptions struct { + Decrypt bool // Whether to decrypt secrets + Verbose bool // Whether to show verbose error output + Export bool // Whether to use export format (export KEY=value vs KEY=value) + OutputFunc func(string) // Callback function for handling output +} + // Runtime encapsulates all core Windsor CLI runtime dependencies. type Runtime struct { Dependencies - Shims *Shims - err error + Shims *Shims + EnvVars map[string]string + EnvAliases map[string]string + err error } // ============================================================================= @@ -180,15 +191,6 @@ func (r *Runtime) HandleSessionReset() *Runtime { shouldReset = true } - if !shouldReset && r.ConfigHandler != nil { - currentContext := r.ConfigHandler.GetContext() - envContext := os.Getenv("WINDSOR_CONTEXT") - if envContext != "" && envContext != currentContext { - shouldReset = true - } - } else if r.ConfigHandler == nil { - } - if shouldReset { r.Shell.Reset() if err := os.Setenv("NO_CACHE", "true"); err != nil { @@ -199,6 +201,79 @@ func (r *Runtime) HandleSessionReset() *Runtime { return r } + +// PrintEnvVars renders and prints the environment variables that were previously collected +// and stored in r.EnvVars using the shell's RenderEnvVars method. The EnvVarsOptions parameter +// controls export formatting and provides an output callback. This method should be called +// after LoadEnvVars to print the collected environment variables. +func (r *Runtime) PrintEnvVars(opts EnvVarsOptions) *Runtime { + if r.err != nil { + return r + } + + if len(r.EnvVars) > 0 { + output := r.Shell.RenderEnvVars(r.EnvVars, opts.Export) + opts.OutputFunc(output) + } + + return r +} + +// PrintAliases prints all collected aliases using the shell's RenderAliases method. +// The outputFunc callback is invoked with the rendered aliases string output. +// If any error occurs during alias retrieval, the Runtime error state is updated +// and the original instance is returned unmodified. +func (r *Runtime) PrintAliases(outputFunc func(string)) *Runtime { + if r.err != nil { + return r + } + + allAliases := make(map[string]string) + for _, envPrinter := range r.getAllEnvPrinters() { + aliases, err := envPrinter.GetAlias() + if err != nil { + r.err = fmt.Errorf("error getting aliases: %w", err) + return r + } + maps.Copy(allAliases, aliases) + } + + if len(allAliases) > 0 { + output := r.Shell.RenderAliases(allAliases) + outputFunc(output) + } + + return r +} + +// ExecutePostEnvHook executes post-environment hooks for all environment printers. +// The Verbose flag controls whether errors are reported. Returns the Runtime instance +// with error state updated if any step fails. +func (r *Runtime) ExecutePostEnvHook(verbose bool) *Runtime { + if r.err != nil { + return r + } + + var firstError error + + printers := r.getAllEnvPrinters() + + for _, printer := range printers { + if printer != nil { + if err := printer.PostEnvHook(); err != nil && firstError == nil { + firstError = err + } + } + } + + if firstError != nil && verbose { + r.err = fmt.Errorf("failed to execute post env hooks: %w", firstError) + return r + } + + return r +} + // CheckTrustedDirectory checks if the current directory is trusted using the shell's // CheckTrustedDirectory method. Returns the Runtime instance with updated error state. func (r *Runtime) CheckTrustedDirectory() *Runtime { @@ -217,3 +292,38 @@ func (r *Runtime) CheckTrustedDirectory() *Runtime { return r } + +// ============================================================================= +// Private Methods +// ============================================================================= + +// 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. +func (r *Runtime) getAllEnvPrinters() []env.EnvPrinter { + const expectedPrinterCount = 7 + _ = [expectedPrinterCount]struct{}{} + + allPrinters := []env.EnvPrinter{ + r.EnvPrinters.AwsEnv, + r.EnvPrinters.AzureEnv, + r.EnvPrinters.DockerEnv, + r.EnvPrinters.KubeEnv, + r.EnvPrinters.TalosEnv, + r.EnvPrinters.TerraformEnv, + r.EnvPrinters.WindsorEnv, + } + + var printers []env.EnvPrinter + for _, printer := range allPrinters { + if printer != nil { + printers = append(printers, printer) + } + } + + if len(printers) > 0 && printers[len(printers)-1] != r.EnvPrinters.WindsorEnv { + panic("WindsorEnv must be the last printer in the list") + } + + return printers +} diff --git a/pkg/runtime/runtime_loaders.go b/pkg/runtime/runtime_loaders.go index f7a7205e0..48d414f06 100644 --- a/pkg/runtime/runtime_loaders.go +++ b/pkg/runtime/runtime_loaders.go @@ -2,6 +2,8 @@ package runtime import ( "fmt" + "maps" + "os" "path/filepath" secretsConfigType "github.com/windsorcli/cli/api/v1alpha1/secrets" @@ -26,18 +28,27 @@ func (r *Runtime) LoadShell() *Runtime { } if r.Shell == nil { - r.Shell = shell.NewDefaultShell(r.Injector) + if existingShell := r.Injector.Resolve("shell"); existingShell != nil { + if shellInstance, ok := existingShell.(shell.Shell); ok { + r.Shell = shellInstance + } else { + r.Shell = shell.NewDefaultShell(r.Injector) + } + } else { + r.Shell = shell.NewDefaultShell(r.Injector) + } } r.Injector.Register("shell", r.Shell) return r } -// LoadConfig loads and initializes the configuration handler dependency and fully loads -// all configuration data into memory. It creates a new ConfigHandler if none exists, initializes -// it with required dependencies, and then loads all configuration sources (schema defaults, -// root windsor.yaml context section, context-specific windsor.yaml/yml files, and values.yaml) -// into the internal data map. This method replaces the previous LoadConfigHandler method -// by combining handler creation/initialization with actual configuration loading. +// LoadConfig initializes the configuration handler dependency and loads all configuration sources +// into memory for use by the runtime. This includes creating a new ConfigHandler if one does not exist, +// initializing it with required dependencies, and loading configuration from schema defaults, the root +// windsor.yaml context section, context-specific windsor.yaml/yml files, and values.yaml. The method +// registers the ConfigHandler with the injector, and returns the Runtime instance with any error set if +// initialization or configuration loading fails. This method supersedes the previous LoadConfigHandler by +// combining handler instantiation, initialization, and configuration loading in one step. func (r *Runtime) LoadConfig() *Runtime { if r.err != nil { return r @@ -48,7 +59,15 @@ func (r *Runtime) LoadConfig() *Runtime { } if r.ConfigHandler == nil { - r.ConfigHandler = config.NewConfigHandler(r.Injector) + if existingConfigHandler := r.Injector.Resolve("configHandler"); existingConfigHandler != nil { + if configHandlerInstance, ok := existingConfigHandler.(config.ConfigHandler); ok { + r.ConfigHandler = configHandlerInstance + } else { + r.ConfigHandler = config.NewConfigHandler(r.Injector) + } + } else { + r.ConfigHandler = config.NewConfigHandler(r.Injector) + } } r.Injector.Register("configHandler", r.ConfigHandler) if err := r.ConfigHandler.Initialize(); err != nil { @@ -62,48 +81,6 @@ func (r *Runtime) LoadConfig() *Runtime { return r } -// LoadEnvPrinters loads and initializes the environment printers. -func (r *Runtime) LoadEnvPrinters() *Runtime { - if r.err != nil { - return r - } - if r.ConfigHandler == nil { - r.err = fmt.Errorf("config handler not loaded - call LoadConfig() first") - return r - } - if r.EnvPrinters.AwsEnv == nil && r.ConfigHandler.GetBool("aws.enabled", false) { - r.EnvPrinters.AwsEnv = env.NewAwsEnvPrinter(r.Injector) - r.Injector.Register("awsEnv", r.EnvPrinters.AwsEnv) - } - if r.EnvPrinters.AzureEnv == nil && r.ConfigHandler.GetBool("azure.enabled", false) { - r.EnvPrinters.AzureEnv = env.NewAzureEnvPrinter(r.Injector) - r.Injector.Register("azureEnv", r.EnvPrinters.AzureEnv) - } - if r.EnvPrinters.DockerEnv == nil && r.ConfigHandler.GetBool("docker.enabled", false) { - r.EnvPrinters.DockerEnv = env.NewDockerEnvPrinter(r.Injector) - r.Injector.Register("dockerEnv", r.EnvPrinters.DockerEnv) - } - if r.EnvPrinters.KubeEnv == nil && r.ConfigHandler.GetBool("cluster.enabled", false) { - r.EnvPrinters.KubeEnv = env.NewKubeEnvPrinter(r.Injector) - r.Injector.Register("kubeEnv", r.EnvPrinters.KubeEnv) - } - if r.EnvPrinters.TalosEnv == nil && - (r.ConfigHandler.GetString("cluster.driver", "") == "talos" || - r.ConfigHandler.GetString("cluster.driver", "") == "omni") { - r.EnvPrinters.TalosEnv = env.NewTalosEnvPrinter(r.Injector) - r.Injector.Register("talosEnv", r.EnvPrinters.TalosEnv) - } - if r.EnvPrinters.TerraformEnv == nil && r.ConfigHandler.GetBool("terraform.enabled", false) { - r.EnvPrinters.TerraformEnv = env.NewTerraformEnvPrinter(r.Injector) - r.Injector.Register("terraformEnv", r.EnvPrinters.TerraformEnv) - } - if r.EnvPrinters.WindsorEnv == nil { - r.EnvPrinters.WindsorEnv = env.NewWindsorEnvPrinter(r.Injector) - r.Injector.Register("windsorEnv", r.EnvPrinters.WindsorEnv) - } - return r -} - // LoadSecretsProviders loads and initializes the secrets providers using configuration and environment. // It detects SOPS and 1Password vaults as in BasePipeline.withSecretsProviders. func (r *Runtime) LoadSecretsProviders() *Runtime { @@ -164,25 +141,18 @@ func (r *Runtime) LoadKubernetes() *Runtime { r.err = fmt.Errorf("config handler not loaded - call LoadConfig() first") return r } - if r.Injector.Resolve("kubernetesClient") == nil { - kubernetesClient := kubernetes.NewDynamicKubernetesClient() - r.Injector.Register("kubernetesClient", kubernetesClient) - } driver := r.ConfigHandler.GetString("cluster.driver") - if driver == "" { - return r - } - if driver == "talos" { - if r.ClusterClient == nil { - r.ClusterClient = cluster.NewTalosClusterClient(r.Injector) - r.Injector.Register("clusterClient", r.ClusterClient) - } - } else { + if driver != "" && driver != "talos" { r.err = fmt.Errorf("unsupported cluster driver: %s", driver) return r } + if r.Injector.Resolve("kubernetesClient") == nil { + kubernetesClient := kubernetes.NewDynamicKubernetesClient() + r.Injector.Register("kubernetesClient", kubernetesClient) + } + if r.K8sManager == nil { r.K8sManager = kubernetes.NewKubernetesManager(r.Injector) } @@ -191,6 +161,14 @@ func (r *Runtime) LoadKubernetes() *Runtime { r.err = fmt.Errorf("failed to initialize kubernetes manager: %w", err) return r } + + if driver == "talos" { + if r.ClusterClient == nil { + r.ClusterClient = cluster.NewTalosClusterClient(r.Injector) + r.Injector.Register("clusterClient", r.ClusterClient) + } + } + return r } @@ -230,3 +208,113 @@ func (r *Runtime) LoadBlueprint() *Runtime { } return r } + +// LoadEnvVars loads environment variables and injects them into the process environment. +// It initializes secret providers if Decrypt is true, and aggregates environment variables +// and aliases from all enabled environment printers. Environment variables are injected +// into the current process, and aliases are collected for later use. The Verbose flag controls +// whether secret loading errors are reported. Returns the Runtime instance with the error +// state updated if any failure occurs during processing. +func (r *Runtime) LoadEnvVars(opts EnvVarsOptions) *Runtime { + if r.err != nil { + return r + } + if r.ConfigHandler == nil { + r.err = fmt.Errorf("config handler not loaded - call LoadConfig() first") + return r + } + + if r.EnvPrinters.AwsEnv == nil && r.ConfigHandler.GetBool("aws.enabled", false) { + r.EnvPrinters.AwsEnv = env.NewAwsEnvPrinter(r.Injector) + r.Injector.Register("awsEnv", r.EnvPrinters.AwsEnv) + } + if r.EnvPrinters.AzureEnv == nil && r.ConfigHandler.GetBool("azure.enabled", false) { + r.EnvPrinters.AzureEnv = env.NewAzureEnvPrinter(r.Injector) + r.Injector.Register("azureEnv", r.EnvPrinters.AzureEnv) + } + if r.EnvPrinters.DockerEnv == nil && r.ConfigHandler.GetBool("docker.enabled", false) { + r.EnvPrinters.DockerEnv = env.NewDockerEnvPrinter(r.Injector) + r.Injector.Register("dockerEnv", r.EnvPrinters.DockerEnv) + } + if r.EnvPrinters.KubeEnv == nil && r.ConfigHandler.GetBool("cluster.enabled", false) { + r.EnvPrinters.KubeEnv = env.NewKubeEnvPrinter(r.Injector) + r.Injector.Register("kubeEnv", r.EnvPrinters.KubeEnv) + } + if r.EnvPrinters.TalosEnv == nil && + (r.ConfigHandler.GetString("cluster.driver", "") == "talos" || + r.ConfigHandler.GetString("cluster.driver", "") == "omni") { + r.EnvPrinters.TalosEnv = env.NewTalosEnvPrinter(r.Injector) + r.Injector.Register("talosEnv", r.EnvPrinters.TalosEnv) + } + if r.EnvPrinters.TerraformEnv == nil && r.ConfigHandler.GetBool("terraform.enabled", false) { + r.EnvPrinters.TerraformEnv = env.NewTerraformEnvPrinter(r.Injector) + r.Injector.Register("terraformEnv", r.EnvPrinters.TerraformEnv) + } + if r.EnvPrinters.WindsorEnv == nil { + r.EnvPrinters.WindsorEnv = env.NewWindsorEnvPrinter(r.Injector) + r.Injector.Register("windsorEnv", r.EnvPrinters.WindsorEnv) + } + + for _, printer := range r.getAllEnvPrinters() { + if printer != nil { + if err := printer.Initialize(); err != nil { + r.err = fmt.Errorf("failed to initialize env printer: %w", err) + return r + } + } + } + + if opts.Decrypt && (r.SecretsProviders.Sops != nil || r.SecretsProviders.Onepassword != nil) { + providers := []secrets.SecretsProvider{ + r.SecretsProviders.Sops, + r.SecretsProviders.Onepassword, + } + for _, provider := range providers { + if provider != nil { + if err := provider.LoadSecrets(); err != nil { + if opts.Verbose { + r.err = fmt.Errorf("failed to load secrets: %w", err) + return r + } + return r + } + } + } + } + + allEnvVars := make(map[string]string) + allAliases := make(map[string]string) + + printers := r.getAllEnvPrinters() + + for _, printer := range printers { + if printer != nil { + envVars, err := printer.GetEnvVars() + if err != nil { + r.err = fmt.Errorf("error getting environment variables: %w", err) + return r + } + maps.Copy(allEnvVars, envVars) + + aliases, err := printer.GetAlias() + if err != nil { + r.err = fmt.Errorf("error getting aliases: %w", err) + return r + } + maps.Copy(allAliases, aliases) + } + } + + r.EnvVars = allEnvVars + + for key, value := range allEnvVars { + if err := os.Setenv(key, value); err != nil { + r.err = fmt.Errorf("error setting environment variable %s: %w", key, err) + return r + } + } + + r.EnvAliases = allAliases + + return r +} diff --git a/pkg/runtime/runtime_loaders_test.go b/pkg/runtime/runtime_loaders_test.go index 411330b6b..c2a0fdfd2 100644 --- a/pkg/runtime/runtime_loaders_test.go +++ b/pkg/runtime/runtime_loaders_test.go @@ -216,340 +216,6 @@ func TestRuntime_LoadConfig(t *testing.T) { }) } -func TestRuntime_LoadEnvPrinters(t *testing.T) { - t.Run("LoadsEnvPrintersSuccessfully", func(t *testing.T) { - // Given a runtime with loaded shell and config handler - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // When loading env printers - result := runtime.LoadEnvPrinters() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadEnvPrinters to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And WindsorEnv should always be loaded - if runtime.EnvPrinters.WindsorEnv == nil { - t.Error("Expected WindsorEnv to be loaded") - } - - // And WindsorEnv should be registered in injector - resolvedWindsorEnv := runtime.Injector.Resolve("windsorEnv") - if resolvedWindsorEnv == nil { - t.Error("Expected WindsorEnv to be registered in injector") - } - }) - - t.Run("ReturnsErrorWhenConfigHandlerNotLoaded", func(t *testing.T) { - // Given a runtime without loaded config handler (no pre-loaded dependencies) - runtime := NewRuntime() - - // When loading env printers - result := runtime.LoadEnvPrinters() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadEnvPrinters to return the same runtime instance") - } - - // And error should be set - if runtime.err == nil { - t.Error("Expected error when config handler not loaded") - } - - expectedError := "config handler not loaded - call LoadConfig() first" - if runtime.err.Error() != expectedError { - t.Errorf("Expected error %q, got %q", expectedError, runtime.err.Error()) - } - }) - - t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { - // Given a runtime with an existing error (no pre-loaded dependencies) - runtime := NewRuntime() - runtime.err = errors.New("existing error") - - // When loading env printers - result := runtime.LoadEnvPrinters() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadEnvPrinters to return the same runtime instance") - } - - // And original error should be preserved - if runtime.err.Error() != "existing error" { - t.Errorf("Expected original error to be preserved, got %v", runtime.err) - } - - // And no env printers should be loaded - if runtime.EnvPrinters.WindsorEnv != nil { - t.Error("Expected no env printers to be loaded when error exists") - } - }) - - t.Run("LoadsOnlyEnabledEnvPrinters", func(t *testing.T) { - // Given a runtime with loaded shell and config handler with specific enabled features - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetBoolFunc = func(key string, defaultValue ...bool) bool { - switch key { - case "aws.enabled": - return true - case "azure.enabled": - return false - case "docker.enabled": - return true - case "cluster.enabled": - return false - case "terraform.enabled": - return true - default: - if len(defaultValue) > 0 { - return defaultValue[0] - } - return false - } - } - mocks.ConfigHandler.(*config.MockConfigHandler).GetStringFunc = func(key string, defaultValue ...string) string { - if key == "cluster.driver" { - return "kubernetes" - } - if len(defaultValue) > 0 { - return defaultValue[0] - } - return "" - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // When loading env printers - result := runtime.LoadEnvPrinters() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadEnvPrinters to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And enabled env printers should be loaded - if runtime.EnvPrinters.AwsEnv == nil { - t.Error("Expected AwsEnv to be loaded when enabled") - } - if runtime.EnvPrinters.DockerEnv == nil { - t.Error("Expected DockerEnv to be loaded when enabled") - } - if runtime.EnvPrinters.TerraformEnv == nil { - t.Error("Expected TerraformEnv to be loaded when enabled") - } - - // And disabled env printers should not be loaded - if runtime.EnvPrinters.AzureEnv != nil { - t.Error("Expected AzureEnv to not be loaded when disabled") - } - if runtime.EnvPrinters.KubeEnv != nil { - t.Error("Expected KubeEnv to not be loaded when disabled") - } - if runtime.EnvPrinters.TalosEnv != nil { - t.Error("Expected TalosEnv to not be loaded when cluster driver is not talos/omni") - } - - // And WindsorEnv should always be loaded - if runtime.EnvPrinters.WindsorEnv == nil { - t.Error("Expected WindsorEnv to be loaded") - } - }) - - t.Run("LoadsWindsorEnvPrinterAlways", func(t *testing.T) { - // Given a runtime with loaded shell and config handler - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // When loading env printers - result := runtime.LoadEnvPrinters() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadEnvPrinters to return the same runtime instance") - } - - // And WindsorEnv should always be loaded regardless of config - if runtime.EnvPrinters.WindsorEnv == nil { - t.Error("Expected WindsorEnv to be loaded") - } - - // And WindsorEnv should be registered in injector - resolvedWindsorEnv := runtime.Injector.Resolve("windsorEnv") - if resolvedWindsorEnv == nil { - t.Error("Expected WindsorEnv to be registered in injector") - } - }) - - t.Run("LoadsTalosEnvPrinterForTalosDriver", func(t *testing.T) { - // Given a runtime with loaded shell and config handler with talos driver - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetStringFunc = func(key string, defaultValue ...string) string { - if key == "cluster.driver" { - return "talos" - } - if len(defaultValue) > 0 { - return defaultValue[0] - } - return "" - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // When loading env printers - result := runtime.LoadEnvPrinters() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadEnvPrinters to return the same runtime instance") - } - - // And TalosEnv should be loaded for talos driver - if runtime.EnvPrinters.TalosEnv == nil { - t.Error("Expected TalosEnv to be loaded for talos driver") - } - - // And TalosEnv should be registered in injector - resolvedTalosEnv := runtime.Injector.Resolve("talosEnv") - if resolvedTalosEnv == nil { - t.Error("Expected TalosEnv to be registered in injector") - } - }) - - t.Run("LoadsTalosEnvPrinterForOmniDriver", func(t *testing.T) { - // Given a runtime with loaded shell and config handler with omni driver - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetStringFunc = func(key string, defaultValue ...string) string { - if key == "cluster.driver" { - return "omni" - } - if len(defaultValue) > 0 { - return defaultValue[0] - } - return "" - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // When loading env printers - result := runtime.LoadEnvPrinters() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadEnvPrinters to return the same runtime instance") - } - - // And TalosEnv should be loaded for omni driver - if runtime.EnvPrinters.TalosEnv == nil { - t.Error("Expected TalosEnv to be loaded for omni driver") - } - - // And TalosEnv should be registered in injector - resolvedTalosEnv := runtime.Injector.Resolve("talosEnv") - if resolvedTalosEnv == nil { - t.Error("Expected TalosEnv to be registered in injector") - } - }) - - t.Run("LoadsAzureEnvPrinterWhenEnabled", func(t *testing.T) { - // Given a runtime with loaded shell and config handler with azure enabled - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetBoolFunc = func(key string, defaultValue ...bool) bool { - if key == "azure.enabled" { - return true - } - if len(defaultValue) > 0 { - return defaultValue[0] - } - return false - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // When loading env printers - result := runtime.LoadEnvPrinters() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadEnvPrinters to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And AzureEnv should be loaded when enabled - if runtime.EnvPrinters.AzureEnv == nil { - t.Error("Expected AzureEnv to be loaded when enabled") - } - - // And AzureEnv should be registered in injector - resolvedAzureEnv := runtime.Injector.Resolve("azureEnv") - if resolvedAzureEnv == nil { - t.Error("Expected AzureEnv to be registered in injector") - } - - // And WindsorEnv should always be loaded - if runtime.EnvPrinters.WindsorEnv == nil { - t.Error("Expected WindsorEnv to be loaded") - } - }) - - t.Run("LoadsKubeEnvPrinterWhenEnabled", func(t *testing.T) { - // Given a runtime with loaded shell and config handler with cluster enabled - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetBoolFunc = func(key string, defaultValue ...bool) bool { - if key == "cluster.enabled" { - return true - } - if len(defaultValue) > 0 { - return defaultValue[0] - } - return false - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // When loading env printers - result := runtime.LoadEnvPrinters() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadEnvPrinters to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And KubeEnv should be loaded when cluster is enabled - if runtime.EnvPrinters.KubeEnv == nil { - t.Error("Expected KubeEnv to be loaded when cluster is enabled") - } - - // And KubeEnv should be registered in injector - resolvedKubeEnv := runtime.Injector.Resolve("kubeEnv") - if resolvedKubeEnv == nil { - t.Error("Expected KubeEnv to be registered in injector") - } - - // And WindsorEnv should always be loaded - if runtime.EnvPrinters.WindsorEnv == nil { - t.Error("Expected WindsorEnv to be loaded") - } - }) -} - func TestRuntime_LoadSecretsProviders(t *testing.T) { t.Run("LoadsSecretsProvidersSuccessfully", func(t *testing.T) { // Given a runtime with loaded shell and config handler @@ -1098,6 +764,56 @@ func TestRuntime_LoadKubernetes(t *testing.T) { } }) + t.Run("LoadsKubernetesWithEmptyClusterDriver", func(t *testing.T) { + // Given a runtime with loaded config handler + mocks := setupMocks(t) + runtime := NewRuntime(mocks).LoadShell().LoadConfig() + + // And mock config handler returns empty string for cluster driver (generic k8s) + mockConfigHandler := runtime.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "cluster.driver" { + return "" + } + return "mock-string" + } + + // When loading kubernetes + result := runtime.LoadKubernetes() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected LoadKubernetes to return the same runtime instance") + } + + // And kubernetes client should be registered in injector + kubernetesClient := runtime.Injector.Resolve("kubernetesClient") + if kubernetesClient == nil { + t.Error("Expected kubernetes client to be registered in injector") + } + + // And kubernetes manager should be loaded + if runtime.K8sManager == nil { + t.Error("Expected kubernetes manager to be loaded") + } + + // And kubernetes manager should be registered in injector + kubernetesManager := runtime.Injector.Resolve("kubernetesManager") + if kubernetesManager == nil { + t.Error("Expected kubernetes manager to be registered in injector") + } + + // And cluster client should NOT be loaded (no specific cluster driver) + if runtime.ClusterClient != nil { + t.Error("Expected cluster client to not be loaded when no cluster driver specified") + } + + // And no error should be set + if runtime.err != nil { + t.Errorf("Expected no error, got %v", runtime.err) + } + }) + t.Run("PropagatesKubernetesManagerInitializationError", func(t *testing.T) { // Given a runtime with loaded config handler mocks := setupMocks(t) diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go index 2137ed8d7..da4109ee3 100644 --- a/pkg/runtime/runtime_test.go +++ b/pkg/runtime/runtime_test.go @@ -3,10 +3,12 @@ package runtime import ( "errors" "os" + "reflect" "strings" "testing" "github.com/windsorcli/cli/pkg/config" + "github.com/windsorcli/cli/pkg/env" "github.com/windsorcli/cli/pkg/shell" ) @@ -683,7 +685,7 @@ func TestRuntime_HandleSessionReset(t *testing.T) { os.Unsetenv("NO_CACHE") }) - t.Run("ResetsWhenContextChanged", func(t *testing.T) { + t.Run("DoesNotResetWhenContextChanged", func(t *testing.T) { // Given a runtime with loaded shell, config handler, and session token mocks := setupMocks(t) runtime := NewRuntime(mocks).LoadShell().LoadConfig() @@ -739,18 +741,15 @@ func TestRuntime_HandleSessionReset(t *testing.T) { t.Errorf("Expected no error, got %v", runtime.err) } - // And reset should be called (context change logic is present in current implementation) - if !resetCalled { - t.Error("Expected shell reset to be called when context changed") + // And reset should NOT be called + if resetCalled { + t.Error("Expected shell reset to NOT be called when context changed (logic not present)") } - // And NO_CACHE should be set - if os.Getenv("NO_CACHE") != "true" { - t.Error("Expected NO_CACHE to be set to true when context changed") + // And NO_CACHE should NOT be set + if os.Getenv("NO_CACHE") == "true" { + t.Error("Expected NO_CACHE to NOT be set when context changed (logic not present)") } - - // Clean up NO_CACHE - os.Unsetenv("NO_CACHE") }) t.Run("DoesNotResetWhenNoResetNeeded", func(t *testing.T) { @@ -1096,3 +1095,578 @@ func TestRuntime_CheckTrustedDirectory(t *testing.T) { } }) } + +func TestRuntime_PrintEnvVars(t *testing.T) { + t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { + // Given a runtime with an existing error + runtime := NewRuntime() + expectedError := errors.New("existing error") + runtime.err = expectedError + + opts := EnvVarsOptions{ + OutputFunc: func(string) {}, + } + + // When printing environment variables + result := runtime.PrintEnvVars(opts) + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected PrintEnvVars to return the same runtime instance") + } + + // And original error should be preserved + if runtime.err != expectedError { + t.Errorf("Expected original error to be preserved, got %v", runtime.err) + } + }) + + t.Run("PrintsEnvVarsSuccessfully", func(t *testing.T) { + // Given a runtime with loaded shell and pre-populated environment variables + mocks := setupMocks(t) + runtime := NewRuntime(mocks).LoadShell() + + // Pre-populate r.EnvVars (simulating what LoadEnvVars would do) + runtime.EnvVars = map[string]string{ + "VAR1": "value1", + "VAR2": "value2", + "VAR3": "value3", + } + + // Track output + var output string + opts := EnvVarsOptions{ + Export: true, + OutputFunc: func(s string) { output = s }, + } + + // Mock shell RenderEnvVars + expectedEnvVars := map[string]string{ + "VAR1": "value1", + "VAR2": "value2", + "VAR3": "value3", + } + mocks.Shell.(*shell.MockShell).RenderEnvVarsFunc = func(envVars map[string]string, export bool) string { + if !reflect.DeepEqual(envVars, expectedEnvVars) { + t.Errorf("Expected env vars %v, got %v", expectedEnvVars, envVars) + } + if !export { + t.Error("Expected export to be true") + } + return "export VAR1=value1\nexport VAR2=value2\nexport VAR3=value3" + } + + // When printing environment variables + result := runtime.PrintEnvVars(opts) + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected PrintEnvVars to return the same runtime instance") + } + + // And no error should be set + if runtime.err != nil { + t.Errorf("Expected no error, got %v", runtime.err) + } + + // And output should be captured + expectedOutput := "export VAR1=value1\nexport VAR2=value2\nexport VAR3=value3" + if output != expectedOutput { + t.Errorf("Expected output %q, got %q", expectedOutput, output) + } + }) + + t.Run("HandlesEmptyEnvVars", func(t *testing.T) { + // Given a runtime with loaded shell and no environment printers + mocks := setupMocks(t) + runtime := NewRuntime(mocks).LoadShell() + + // Track if output function is called + outputCalled := false + opts := EnvVarsOptions{ + OutputFunc: func(string) { outputCalled = true }, + } + + // When printing environment variables + result := runtime.PrintEnvVars(opts) + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected PrintEnvVars to return the same runtime instance") + } + + // And no error should be set + if runtime.err != nil { + t.Errorf("Expected no error, got %v", runtime.err) + } + + // And output function should not be called + if outputCalled { + t.Error("Expected output function to not be called when no env vars") + } + }) + +} + +func TestRuntime_PrintAliases(t *testing.T) { + t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { + // Given a runtime with an existing error + runtime := NewRuntime() + expectedError := errors.New("existing error") + runtime.err = expectedError + + // When printing aliases + result := runtime.PrintAliases(func(string) {}) + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected PrintAliases to return the same runtime instance") + } + + // And original error should be preserved + if runtime.err != expectedError { + t.Errorf("Expected original error to be preserved, got %v", runtime.err) + } + }) + + t.Run("PrintsAliasesSuccessfully", func(t *testing.T) { + // Given a runtime with loaded shell and environment printers + mocks := setupMocks(t) + runtime := NewRuntime(mocks).LoadShell() + + // Set up mock environment printers + mockPrinter1 := env.NewMockEnvPrinter() + mockPrinter1.GetAliasFunc = func() (map[string]string, error) { + return map[string]string{"alias1": "command1", "alias2": "command2"}, nil + } + + mockPrinter2 := env.NewMockEnvPrinter() + mockPrinter2.GetAliasFunc = func() (map[string]string, error) { + return map[string]string{"alias3": "command3"}, nil + } + + runtime.EnvPrinters.AwsEnv = mockPrinter1 + runtime.EnvPrinters.WindsorEnv = mockPrinter2 + + // Track output + var output string + outputFunc := func(s string) { output = s } + + // Mock shell RenderAliases + expectedAliases := map[string]string{ + "alias1": "command1", + "alias2": "command2", + "alias3": "command3", + } + mocks.Shell.(*shell.MockShell).RenderAliasesFunc = func(aliases map[string]string) string { + if !reflect.DeepEqual(aliases, expectedAliases) { + t.Errorf("Expected aliases %v, got %v", expectedAliases, aliases) + } + return "alias alias1='command1'\nalias alias2='command2'\nalias alias3='command3'" + } + + // When printing aliases + result := runtime.PrintAliases(outputFunc) + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected PrintAliases to return the same runtime instance") + } + + // And no error should be set + if runtime.err != nil { + t.Errorf("Expected no error, got %v", runtime.err) + } + + // And output should be captured + expectedOutput := "alias alias1='command1'\nalias alias2='command2'\nalias alias3='command3'" + if output != expectedOutput { + t.Errorf("Expected output %q, got %q", expectedOutput, output) + } + }) + + t.Run("HandlesEmptyAliases", func(t *testing.T) { + // Given a runtime with loaded shell and no environment printers + mocks := setupMocks(t) + runtime := NewRuntime(mocks).LoadShell() + + // Track if output function is called + outputCalled := false + outputFunc := func(string) { outputCalled = true } + + // When printing aliases + result := runtime.PrintAliases(outputFunc) + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected PrintAliases to return the same runtime instance") + } + + // And no error should be set + if runtime.err != nil { + t.Errorf("Expected no error, got %v", runtime.err) + } + + // And output function should not be called + if outputCalled { + t.Error("Expected output function to not be called when no aliases") + } + }) + + t.Run("PropagatesAliasError", func(t *testing.T) { + // Given a runtime with loaded shell and environment printer that returns error + mocks := setupMocks(t) + runtime := NewRuntime(mocks).LoadShell() + + // Set up mock environment printer that returns error + mockPrinter := env.NewMockEnvPrinter() + expectedError := errors.New("alias error") + mockPrinter.GetAliasFunc = func() (map[string]string, error) { + return nil, expectedError + } + + // Set up WindsorEnv printer to avoid panic + windsorPrinter := env.NewMockEnvPrinter() + windsorPrinter.GetAliasFunc = func() (map[string]string, error) { + return map[string]string{}, nil + } + + runtime.EnvPrinters.AwsEnv = mockPrinter + runtime.EnvPrinters.WindsorEnv = windsorPrinter + + // When printing aliases + result := runtime.PrintAliases(func(string) {}) + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected PrintAliases to return the same runtime instance") + } + + // And error should be set + if runtime.err == nil { + t.Error("Expected error to be set") + } else { + expectedErrorMsg := "error getting aliases: alias error" + if runtime.err.Error() != expectedErrorMsg { + t.Errorf("Expected error %q, got %q", expectedErrorMsg, runtime.err.Error()) + } + } + }) +} + +func TestRuntime_ExecutePostEnvHook(t *testing.T) { + t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { + // Given a runtime with an existing error + runtime := NewRuntime() + expectedError := errors.New("existing error") + runtime.err = expectedError + + // When executing post env hook + result := runtime.ExecutePostEnvHook(true) + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected ExecutePostEnvHook to return the same runtime instance") + } + + // And original error should be preserved + if runtime.err != expectedError { + t.Errorf("Expected original error to be preserved, got %v", runtime.err) + } + }) + + t.Run("ExecutesPostEnvHooksSuccessfully", func(t *testing.T) { + // Given a runtime with environment printers + mocks := setupMocks(t) + runtime := NewRuntime(mocks) + + // Set up mock environment printers + hook1Called := false + hook2Called := false + + mockPrinter1 := env.NewMockEnvPrinter() + mockPrinter1.PostEnvHookFunc = func(directory ...string) error { + hook1Called = true + return nil + } + + mockPrinter2 := env.NewMockEnvPrinter() + mockPrinter2.PostEnvHookFunc = func(directory ...string) error { + hook2Called = true + return nil + } + + runtime.EnvPrinters.AwsEnv = mockPrinter1 + runtime.EnvPrinters.WindsorEnv = mockPrinter2 + + // When executing post env hook + result := runtime.ExecutePostEnvHook(true) + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected ExecutePostEnvHook to return the same runtime instance") + } + + // And no error should be set + if runtime.err != nil { + t.Errorf("Expected no error, got %v", runtime.err) + } + + // And hooks should be called + if !hook1Called { + t.Error("Expected first hook to be called") + } + if !hook2Called { + t.Error("Expected second hook to be called") + } + }) + + t.Run("HandlesHookErrorWithVerboseTrue", func(t *testing.T) { + // Given a runtime with environment printer that returns error + mocks := setupMocks(t) + runtime := NewRuntime(mocks) + + // Set up mock environment printer that returns error + mockPrinter := env.NewMockEnvPrinter() + expectedError := errors.New("hook error") + mockPrinter.PostEnvHookFunc = func(directory ...string) error { + return expectedError + } + + // Set up WindsorEnv printer to avoid panic + windsorPrinter := env.NewMockEnvPrinter() + windsorPrinter.PostEnvHookFunc = func(directory ...string) error { + return nil + } + + runtime.EnvPrinters.AwsEnv = mockPrinter + runtime.EnvPrinters.WindsorEnv = windsorPrinter + + // When executing post env hook with verbose true + result := runtime.ExecutePostEnvHook(true) + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected ExecutePostEnvHook to return the same runtime instance") + } + + // And error should be set + if runtime.err == nil { + t.Error("Expected error to be set") + } else { + expectedErrorMsg := "failed to execute post env hooks: hook error" + if runtime.err.Error() != expectedErrorMsg { + t.Errorf("Expected error %q, got %q", expectedErrorMsg, runtime.err.Error()) + } + } + }) + + t.Run("HandlesHookErrorWithVerboseFalse", func(t *testing.T) { + // Given a runtime with environment printer that returns error + mocks := setupMocks(t) + runtime := NewRuntime(mocks) + + // Set up mock environment printer that returns error + mockPrinter := env.NewMockEnvPrinter() + expectedError := errors.New("hook error") + mockPrinter.PostEnvHookFunc = func(directory ...string) error { + return expectedError + } + + // Set up WindsorEnv printer to avoid panic + windsorPrinter := env.NewMockEnvPrinter() + windsorPrinter.PostEnvHookFunc = func(directory ...string) error { + return nil + } + + runtime.EnvPrinters.AwsEnv = mockPrinter + runtime.EnvPrinters.WindsorEnv = windsorPrinter + + // When executing post env hook with verbose false + result := runtime.ExecutePostEnvHook(false) + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected ExecutePostEnvHook to return the same runtime instance") + } + + // And no error should be set (verbose false suppresses errors) + if runtime.err != nil { + t.Errorf("Expected no error when verbose false, got %v", runtime.err) + } + }) + + t.Run("HandlesMultipleHookErrors", func(t *testing.T) { + // Given a runtime with multiple environment printers that return errors + mocks := setupMocks(t) + runtime := NewRuntime(mocks) + + // Set up mock environment printers that return errors + mockPrinter1 := env.NewMockEnvPrinter() + error1 := errors.New("hook error 1") + mockPrinter1.PostEnvHookFunc = func(directory ...string) error { + return error1 + } + + mockPrinter2 := env.NewMockEnvPrinter() + error2 := errors.New("hook error 2") + mockPrinter2.PostEnvHookFunc = func(directory ...string) error { + return error2 + } + + runtime.EnvPrinters.AwsEnv = mockPrinter1 + runtime.EnvPrinters.WindsorEnv = mockPrinter2 + + // When executing post env hook + result := runtime.ExecutePostEnvHook(true) + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected ExecutePostEnvHook to return the same runtime instance") + } + + // And error should be set with first error + if runtime.err == nil { + t.Error("Expected error to be set") + } else { + expectedErrorMsg := "failed to execute post env hooks: hook error 1" + if runtime.err.Error() != expectedErrorMsg { + t.Errorf("Expected error %q, got %q", expectedErrorMsg, runtime.err.Error()) + } + } + }) + + t.Run("SkipsNilPrinters", func(t *testing.T) { + // Given a runtime with some nil environment printers + mocks := setupMocks(t) + runtime := NewRuntime(mocks) + + // Set up one mock environment printer + hookCalled := false + mockPrinter := env.NewMockEnvPrinter() + mockPrinter.PostEnvHookFunc = func(directory ...string) error { + hookCalled = true + return nil + } + + // Set up WindsorEnv printer to avoid panic + windsorPrinter := env.NewMockEnvPrinter() + windsorPrinter.PostEnvHookFunc = func(directory ...string) error { + return nil + } + + runtime.EnvPrinters.AwsEnv = mockPrinter + runtime.EnvPrinters.WindsorEnv = windsorPrinter + // Other printers remain nil + + // When executing post env hook + result := runtime.ExecutePostEnvHook(true) + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected ExecutePostEnvHook to return the same runtime instance") + } + + // And no error should be set + if runtime.err != nil { + t.Errorf("Expected no error, got %v", runtime.err) + } + + // And hook should be called + if !hookCalled { + t.Error("Expected hook to be called") + } + }) +} + +func TestRuntime_getAllEnvPrinters(t *testing.T) { + t.Run("ReturnsAllNonNilPrinters", func(t *testing.T) { + // Given a runtime with some environment printers set + mocks := setupMocks(t) + runtime := NewRuntime(mocks) + + // Set up some mock environment printers + mockPrinter1 := env.NewMockEnvPrinter() + mockPrinter2 := env.NewMockEnvPrinter() + mockPrinter3 := env.NewMockEnvPrinter() + + runtime.EnvPrinters.AwsEnv = mockPrinter1 + runtime.EnvPrinters.AzureEnv = mockPrinter2 + runtime.EnvPrinters.WindsorEnv = mockPrinter3 + // Other printers remain nil + + // When getting all environment printers + printers := runtime.getAllEnvPrinters() + + // Then should return only non-nil printers + if len(printers) != 3 { + t.Errorf("Expected 3 printers, got %d", len(printers)) + } + + // And should include the set printers + foundAws := false + foundAzure := false + foundWindsor := false + + for _, printer := range printers { + if printer == mockPrinter1 { + foundAws = true + } + if printer == mockPrinter2 { + foundAzure = true + } + if printer == mockPrinter3 { + foundWindsor = true + } + } + + if !foundAws { + t.Error("Expected AWS printer to be included") + } + if !foundAzure { + t.Error("Expected Azure printer to be included") + } + if !foundWindsor { + t.Error("Expected Windsor printer to be included") + } + }) + + t.Run("EnsuresWindsorEnvIsLast", func(t *testing.T) { + // Given a runtime with WindsorEnv and other printers + mocks := setupMocks(t) + runtime := NewRuntime(mocks) + + // Set up mock environment printers + mockPrinter1 := env.NewMockEnvPrinter() + mockPrinter2 := env.NewMockEnvPrinter() + windsorPrinter := env.NewMockEnvPrinter() + + runtime.EnvPrinters.AwsEnv = mockPrinter1 + runtime.EnvPrinters.AzureEnv = mockPrinter2 + runtime.EnvPrinters.WindsorEnv = windsorPrinter + + // When getting all environment printers + printers := runtime.getAllEnvPrinters() + + // Then WindsorEnv should be last + if len(printers) == 0 { + t.Error("Expected at least one printer") + } else if printers[len(printers)-1] != windsorPrinter { + t.Error("Expected WindsorEnv to be the last printer") + } + }) + + t.Run("ReturnsEmptySliceWhenNoPrinters", func(t *testing.T) { + // Given a runtime with no environment printers set + mocks := setupMocks(t) + runtime := NewRuntime(mocks) + + // When getting all environment printers + printers := runtime.getAllEnvPrinters() + + // Then should return empty slice + if len(printers) != 0 { + t.Errorf("Expected 0 printers, got %d", len(printers)) + } + }) +} diff --git a/pkg/shell/mock_shell.go b/pkg/shell/mock_shell.go index ed2ec7e30..6dc7c9b25 100644 --- a/pkg/shell/mock_shell.go +++ b/pkg/shell/mock_shell.go @@ -18,8 +18,8 @@ import ( type MockShell struct { DefaultShell InitializeFunc func() error - PrintEnvVarsFunc func(envVars map[string]string, export bool) - PrintAliasFunc func(envVars map[string]string) + RenderEnvVarsFunc func(envVars map[string]string, export bool) string + RenderAliasesFunc func(aliases map[string]string) string GetProjectRootFunc func() (string, error) ExecFunc func(command string, args ...string) (string, error) ExecSilentFunc func(command string, args ...string) (string, error) @@ -70,18 +70,20 @@ func (s *MockShell) Initialize() error { return nil } -// PrintEnvVars calls the custom PrintEnvVarsFunc if provided. -func (s *MockShell) PrintEnvVars(envVars map[string]string, export bool) { - if s.PrintEnvVarsFunc != nil { - s.PrintEnvVarsFunc(envVars, export) +// RenderEnvVars calls the custom RenderEnvVarsFunc if provided. +func (s *MockShell) RenderEnvVars(envVars map[string]string, export bool) string { + if s.RenderEnvVarsFunc != nil { + return s.RenderEnvVarsFunc(envVars, export) } + return "" } -// PrintAlias calls the custom PrintAliasFunc if provided. -func (s *MockShell) PrintAlias(envVars map[string]string) { - if s.PrintAliasFunc != nil { - s.PrintAliasFunc(envVars) +// RenderAliases calls the custom RenderAliasesFunc if provided. +func (s *MockShell) RenderAliases(aliases map[string]string) string { + if s.RenderAliasesFunc != nil { + return s.RenderAliasesFunc(aliases) } + return "" } // GetProjectRoot calls the custom GetProjectRootFunc if provided. diff --git a/pkg/shell/mock_shell_test.go b/pkg/shell/mock_shell_test.go index c9948ef9f..8caad1a2f 100644 --- a/pkg/shell/mock_shell_test.go +++ b/pkg/shell/mock_shell_test.go @@ -1023,70 +1023,6 @@ func TestMockShell_GetSessionToken(t *testing.T) { }) } -func TestMockShell_PrintEnvVars(t *testing.T) { - t.Run("CallsPrintEnvVarsFunc", func(t *testing.T) { - // Given a mock shell with PrintEnvVarsFunc set - mockShell := setupMockShellMocks(t) - called := false - expectedEnvVars := map[string]string{"TEST": "value"} - mockShell.PrintEnvVarsFunc = func(envVars map[string]string, export bool) { - called = true - if envVars["TEST"] != "value" { - t.Errorf("Expected envVars[TEST] = value, got %v", envVars["TEST"]) - } - } - - // When PrintEnvVars is called - mockShell.PrintEnvVars(expectedEnvVars, true) - - // Then the mock function should be called - if !called { - t.Error("Expected PrintEnvVarsFunc to be called") - } - }) - - t.Run("NilFuncDoesNotPanic", func(t *testing.T) { - // Given a mock shell without PrintEnvVarsFunc set - mockShell := setupMockShellMocks(t) - - // When PrintEnvVars is called - // Then it should not panic - mockShell.PrintEnvVars(map[string]string{"TEST": "value"}, false) - }) -} - -func TestMockShell_PrintAlias(t *testing.T) { - t.Run("CallsPrintAliasFunc", func(t *testing.T) { - // Given a mock shell with PrintAliasFunc set - mockShell := setupMockShellMocks(t) - called := false - expectedAliases := map[string]string{"TEST": "value"} - mockShell.PrintAliasFunc = func(aliases map[string]string) { - called = true - if aliases["TEST"] != "value" { - t.Errorf("Expected aliases[TEST] = value, got %v", aliases["TEST"]) - } - } - - // When PrintAlias is called - mockShell.PrintAlias(expectedAliases) - - // Then the mock function should be called - if !called { - t.Error("Expected PrintAliasFunc to be called") - } - }) - - t.Run("NilFuncDoesNotPanic", func(t *testing.T) { - // Given a mock shell without PrintAliasFunc set - mockShell := setupMockShellMocks(t) - - // When PrintAlias is called - // Then it should not panic - mockShell.PrintAlias(map[string]string{"TEST": "value"}) - }) -} - func TestMockShell_Reset(t *testing.T) { t.Run("CallsResetFunc", func(t *testing.T) { // Given a mock shell with ResetFunc set diff --git a/pkg/shell/shell.go b/pkg/shell/shell.go index e512e82f0..4f808f634 100644 --- a/pkg/shell/shell.go +++ b/pkg/shell/shell.go @@ -47,8 +47,8 @@ type HookContext struct { type Shell interface { Initialize() error SetVerbosity(verbose bool) - PrintEnvVars(envVars map[string]string, export bool) - PrintAlias(envVars map[string]string) + RenderEnvVars(envVars map[string]string, export bool) string + RenderAliases(aliases map[string]string) string GetProjectRoot() (string, error) Exec(command string, args ...string) (string, error) ExecSilent(command string, args ...string) (string, error) @@ -633,6 +633,16 @@ func (s *DefaultShell) ResetSessionToken() { s.sessionToken = "" } +// RenderEnvVars returns the rendered environment variables as a string instead of printing them +// The export parameter controls whether to use OS-specific export commands or plain KEY=value format +func (s *DefaultShell) RenderEnvVars(envVars map[string]string, export bool) string { + if export { + return s.renderEnvVarsWithExport(envVars) + } else { + return s.renderEnvVarsPlain(envVars) + } +} + // ============================================================================= // Private Methods // ============================================================================= @@ -668,27 +678,30 @@ func (s *DefaultShell) scrubString(input string) string { // The export parameter controls whether to use OS-specific export commands or plain KEY=value format func (s *DefaultShell) PrintEnvVars(envVars map[string]string, export bool) { if export { - s.printEnvVarsWithExport(envVars) + s.renderEnvVarsWithExport(envVars) } else { - s.printEnvVarsPlain(envVars) + s.renderEnvVarsPlain(envVars) } } -// printEnvVarsPlain prints environment variables in plain KEY=value format, sorted by key. -// If a value is empty, it prints KEY= with no value. Used for non-export output scenarios. -func (s *DefaultShell) printEnvVarsPlain(envVars map[string]string) { +// renderEnvVarsPlain returns environment variables in plain KEY=value format as a string, sorted by key. +// If a value is empty, it returns KEY= with no value. Used for non-export output scenarios. +func (s *DefaultShell) renderEnvVarsPlain(envVars map[string]string) string { keys := make([]string, 0, len(envVars)) for k := range envVars { keys = append(keys, k) } sort.Strings(keys) + + var result strings.Builder for _, k := range keys { if envVars[k] == "" { - fmt.Printf("%s=\n", k) + result.WriteString(fmt.Sprintf("%s=\n", k)) } else { - fmt.Printf("%s=%s\n", k, envVars[k]) + result.WriteString(fmt.Sprintf("%s=%s\n", k, envVars[k])) } } + return result.String() } // Ensure DefaultShell implements the Shell interface diff --git a/pkg/shell/shell_test.go b/pkg/shell/shell_test.go index e07c6640d..ef0d666fa 100644 --- a/pkg/shell/shell_test.go +++ b/pkg/shell/shell_test.go @@ -914,7 +914,7 @@ func TestShell_GetSessionToken(t *testing.T) { } t.Run("Success", func(t *testing.T) { - // Given a shell with session token + // Given a shell with session token and no reset flag shell, mocks := setup(t) expectedToken := "test-token" mocks.Shims.Getenv = func(key string) string { @@ -924,6 +924,16 @@ func TestShell_GetSessionToken(t *testing.T) { return "" } + // Mock Stat to return file not found (no reset flag exists) + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + + // Mock Glob to return empty slice (no session files) + mocks.Shims.Glob = func(pattern string) ([]string, error) { + return []string{}, nil + } + // When getting session token token, err := shell.GetSessionToken() @@ -2262,6 +2272,16 @@ func TestShell_ResetSessionToken(t *testing.T) { return len(b), nil } + // Mock Stat to return file not found (no reset flag exists) + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + + // Mock Glob to return empty slice (no session files) + mocks.Shims.Glob = func(pattern string) ([]string, error) { + return []string{}, nil + } + // When getting session token token, err := shell.GetSessionToken() if err != nil { diff --git a/pkg/shell/unix_shell.go b/pkg/shell/unix_shell.go index f4caf7c72..8b8877883 100644 --- a/pkg/shell/unix_shell.go +++ b/pkg/shell/unix_shell.go @@ -18,39 +18,6 @@ import ( // Public Methods // ============================================================================= -// printEnvVarsWithExport prints environment variables in sorted order using export commands. -// If a variable's value is empty, it prints an unset command instead. -func (s *DefaultShell) printEnvVarsWithExport(envVars map[string]string) { - keys := make([]string, 0, len(envVars)) - for k := range envVars { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - if envVars[k] == "" { - fmt.Printf("unset %s\n", k) - } else { - fmt.Printf("export %s=\"%s\"\n", k, envVars[k]) - } - } -} - -// PrintAlias prints sorted aliases. Empty values print unalias; non-empty print alias with key and value. -func (s *DefaultShell) PrintAlias(aliases map[string]string) { - keys := make([]string, 0, len(aliases)) - for k := range aliases { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - if aliases[k] == "" { - fmt.Printf("unalias %s\n", k) - } else { - fmt.Printf("alias %s=\"%s\"\n", k, aliases[k]) - } - } -} - // UnsetEnvs generates a single unset command for multiple environment variables in Unix shells. // It prints a single 'unset' command with all provided variable names separated by spaces. // If the input slice is empty, no output is produced. @@ -72,3 +39,46 @@ func (s *DefaultShell) UnsetAlias(aliases []string) { fmt.Printf("unalias %s\n", alias) } } + +// RenderAliases returns the rendered aliases as a string using Unix shell syntax +func (s *DefaultShell) RenderAliases(aliases map[string]string) string { + keys := make([]string, 0, len(aliases)) + for k := range aliases { + keys = append(keys, k) + } + sort.Strings(keys) + + var result strings.Builder + for _, k := range keys { + if aliases[k] == "" { + result.WriteString(fmt.Sprintf("unalias %s\n", k)) + } else { + result.WriteString(fmt.Sprintf("alias %s=\"%s\"\n", k, aliases[k])) + } + } + return result.String() +} + +// ============================================================================= +// Private Methods +// ============================================================================= + +// renderEnvVarsWithExport returns environment variables in sorted order using export commands as a string. +// If a variable's value is empty, it returns an unset command instead. +func (s *DefaultShell) renderEnvVarsWithExport(envVars map[string]string) string { + keys := make([]string, 0, len(envVars)) + for k := range envVars { + keys = append(keys, k) + } + sort.Strings(keys) + + var result strings.Builder + for _, k := range keys { + if envVars[k] == "" { + result.WriteString(fmt.Sprintf("unset %s\n", k)) + } else { + result.WriteString(fmt.Sprintf("export %s=\"%s\"\n", k, envVars[k])) + } + } + return result.String() +} diff --git a/pkg/shell/unix_shell_test.go b/pkg/shell/unix_shell_test.go index 4bd27d3aa..a446e6398 100644 --- a/pkg/shell/unix_shell_test.go +++ b/pkg/shell/unix_shell_test.go @@ -4,10 +4,8 @@ package shell import ( - "fmt" "os" "path/filepath" - "strings" "testing" ) @@ -20,38 +18,6 @@ import ( // Test Public Methods // ============================================================================= -// TestDefaultShell_PrintEnvVars tests the PrintEnvVars method on Unix systems -func TestDefaultShell_PrintEnvVars(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { - t.Helper() - mocks := setupMocks(t) - shell := NewDefaultShell(mocks.Injector) - shell.shims = mocks.Shims - return shell, mocks - } - - t.Run("PrintEnvVars", func(t *testing.T) { - // Given a shell with environment variables - shell, _ := setup(t) - envVars := map[string]string{ - "VAR2": "value2", - "VAR1": "value1", - "VAR3": "", - } - expectedOutput := "export VAR1=\"value1\"\nexport VAR2=\"value2\"\nunset VAR3\n" - - // When capturing the output of PrintEnvVars - output := captureStdout(t, func() { - shell.PrintEnvVars(envVars, true) - }) - - // Then the output should match the expected output - if output != expectedOutput { - t.Errorf("PrintEnvVars() output = %v, want %v", output, expectedOutput) - } - }) -} - // TestDefaultShell_GetProjectRoot tests the GetProjectRoot method on Unix systems func TestDefaultShell_GetProjectRoot(t *testing.T) { setup := func(t *testing.T) (*DefaultShell, *Mocks) { @@ -107,63 +73,6 @@ func TestDefaultShell_GetProjectRoot(t *testing.T) { } // TestDefaultShell_PrintAlias tests the PrintAlias method on Unix systems -func TestDefaultShell_PrintAlias(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { - t.Helper() - mocks := setupMocks(t) - shell := NewDefaultShell(mocks.Injector) - shell.shims = mocks.Shims - return shell, mocks - } - - aliasVars := map[string]string{ - "ALIAS1": "command1", - "ALIAS2": "command2", - } - - t.Run("PrintAlias", func(t *testing.T) { - // Given a default shell - shell, _ := setup(t) - - // When capturing the output of PrintAlias - output := captureStdout(t, func() { - shell.PrintAlias(aliasVars) - }) - - // Then the output should contain all expected alias variables - for key, value := range aliasVars { - expectedLine := fmt.Sprintf("alias %s=\"%s\"\n", key, value) - if !strings.Contains(output, expectedLine) { - t.Errorf("PrintAlias() output missing expected line: %v", expectedLine) - } - } - }) - - t.Run("PrintAliasWithEmptyValue", func(t *testing.T) { - // Given a default shell with an alias having an empty value - shell, _ := setup(t) - aliasVarsWithEmpty := map[string]string{ - "ALIAS1": "command1", - "ALIAS2": "", - } - - // When capturing the output of PrintAlias - output := captureStdout(t, func() { - shell.PrintAlias(aliasVarsWithEmpty) - }) - - // Then the output should contain the expected alias and unalias commands - expectedAliasLine := "alias ALIAS1=\"command1\"\n" - expectedUnaliasLine := "unalias ALIAS2\n" - - if !strings.Contains(output, expectedAliasLine) { - t.Errorf("PrintAlias() output missing expected line: %v", expectedAliasLine) - } - if !strings.Contains(output, expectedUnaliasLine) { - t.Errorf("PrintAlias() output missing expected line: %v", expectedUnaliasLine) - } - }) -} // TestDefaultShell_UnsetEnvs tests the UnsetEnvs method on Unix systems func TestDefaultShell_UnsetEnvs(t *testing.T) { diff --git a/pkg/shell/windows_shell.go b/pkg/shell/windows_shell.go index d765fb8a2..ffac7c5eb 100644 --- a/pkg/shell/windows_shell.go +++ b/pkg/shell/windows_shell.go @@ -6,6 +6,7 @@ package shell import ( "fmt" "sort" + "strings" ) // The WindowsShell is a platform-specific implementation of shell operations for Windows systems. @@ -17,47 +18,6 @@ import ( // Public Methods // ============================================================================= -// printEnvVarsWithExport sorts and prints environment variables with PowerShell commands. Empty values trigger a removal command. -func (s *DefaultShell) printEnvVarsWithExport(envVars map[string]string) { - keys := make([]string, 0, len(envVars)) - for k := range envVars { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - if envVars[k] == "" { - fmt.Printf("Remove-Item Env:%s\n", k) - } else { - fmt.Printf("$env:%s='%s'\n", k, envVars[k]) - } - } -} - -// PrintAlias prints the aliases for the shell. -func (s *DefaultShell) PrintAlias(aliases map[string]string) { - // Create a slice to hold the keys of the aliases map - keys := make([]string, 0, len(aliases)) - - // Append each key from the aliases map to the keys slice - for k := range aliases { - keys = append(keys, k) - } - - // Sort the keys slice to ensure the aliases are printed in order - sort.Strings(keys) - - // Iterate over the sorted keys and print the corresponding alias - for _, k := range keys { - if aliases[k] == "" { - // Print command to remove the alias if the value is an empty string - fmt.Printf("Remove-Item Alias:%s\n", k) - } else { - // Print command to set the alias with the key and value - fmt.Printf("Set-Alias -Name %s -Value \"%s\"\n", k, aliases[k]) - } - } -} - // UnsetEnvs generates commands to unset multiple environment variables. // For Windows PowerShell, this produces a Remove-Item command for each environment variable. func (s *DefaultShell) UnsetEnvs(envVars []string) { @@ -83,3 +43,45 @@ func (s *DefaultShell) UnsetAlias(aliases []string) { fmt.Printf("Remove-Item Alias:%s\n", alias) } } + +// RenderAliases returns the rendered aliases as a string using PowerShell syntax +func (s *DefaultShell) RenderAliases(aliases map[string]string) string { + keys := make([]string, 0, len(aliases)) + for k := range aliases { + keys = append(keys, k) + } + sort.Strings(keys) + + var result strings.Builder + for _, k := range keys { + if aliases[k] == "" { + result.WriteString(fmt.Sprintf("Remove-Item Alias:%s\n", k)) + } else { + result.WriteString(fmt.Sprintf("Set-Alias %s '%s'\n", k, aliases[k])) + } + } + return result.String() +} + +// ============================================================================= +// Private Methods +// ============================================================================= + +// renderEnvVarsWithExport returns environment variables with PowerShell commands as a string. Empty values trigger a removal command. +func (s *DefaultShell) renderEnvVarsWithExport(envVars map[string]string) string { + keys := make([]string, 0, len(envVars)) + for k := range envVars { + keys = append(keys, k) + } + sort.Strings(keys) + + var result strings.Builder + for _, k := range keys { + if envVars[k] == "" { + result.WriteString(fmt.Sprintf("Remove-Item Env:%s\n", k)) + } else { + result.WriteString(fmt.Sprintf("$env:%s='%s'\n", k, envVars[k])) + } + } + return result.String() +} diff --git a/pkg/shell/windows_shell_test.go b/pkg/shell/windows_shell_test.go index 45c2e369e..f972f88e9 100644 --- a/pkg/shell/windows_shell_test.go +++ b/pkg/shell/windows_shell_test.go @@ -4,10 +4,8 @@ package shell import ( - "fmt" "os" "path/filepath" - "strings" "testing" ) @@ -20,38 +18,6 @@ import ( // Test Public Methods // ============================================================================= -// TestDefaultShell_PrintEnvVars tests the PrintEnvVars method on Windows systems -func TestDefaultShell_PrintEnvVars(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { - t.Helper() - mocks := setupMocks(t) - shell := NewDefaultShell(mocks.Injector) - shell.shims = mocks.Shims - return shell, mocks - } - - t.Run("PrintEnvVars", func(t *testing.T) { - // Given a shell with environment variables - shell, _ := setup(t) - envVars := map[string]string{ - "VAR2": "value2", - "VAR1": "value1", - "VAR3": "", - } - expectedOutput := "$env:VAR1='value1'\n$env:VAR2='value2'\nRemove-Item Env:VAR3\n" - - // When capturing the output of PrintEnvVars - output := captureStdout(t, func() { - shell.PrintEnvVars(envVars, true) - }) - - // Then the output should match the expected output - if output != expectedOutput { - t.Errorf("PrintEnvVars() output = %v, want %v", output, expectedOutput) - } - }) -} - // TestDefaultShell_GetProjectRoot tests the GetProjectRoot method on Windows systems func TestDefaultShell_GetProjectRoot(t *testing.T) { setup := func(t *testing.T) (*DefaultShell, *Mocks) { @@ -105,63 +71,6 @@ func TestDefaultShell_GetProjectRoot(t *testing.T) { } // TestDefaultShell_PrintAlias tests the PrintAlias method on Windows systems -func TestDefaultShell_PrintAlias(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { - t.Helper() - mocks := setupMocks(t) - shell := NewDefaultShell(mocks.Injector) - shell.shims = mocks.Shims - return shell, mocks - } - - aliasVars := map[string]string{ - "ALIAS1": "command1", - "ALIAS2": "command2", - } - - t.Run("PrintAlias", func(t *testing.T) { - // Given a default shell - shell, _ := setup(t) - - // When capturing the output of PrintAlias - output := captureStdout(t, func() { - shell.PrintAlias(aliasVars) - }) - - // Then the output should contain all expected alias variables - for key, value := range aliasVars { - expectedLine := fmt.Sprintf("Set-Alias -Name %s -Value \"%s\"\n", key, value) - if !strings.Contains(output, expectedLine) { - t.Errorf("PrintAlias() output missing expected line: %v", expectedLine) - } - } - }) - - t.Run("PrintAliasWithEmptyValue", func(t *testing.T) { - // Given a default shell with an alias having an empty value - shell, _ := setup(t) - aliasVarsWithEmpty := map[string]string{ - "ALIAS1": "command1", - "ALIAS2": "", - } - - // When capturing the output of PrintAlias - output := captureStdout(t, func() { - shell.PrintAlias(aliasVarsWithEmpty) - }) - - // Then the output should contain the expected alias and remove alias commands - expectedAliasLine := "Set-Alias -Name ALIAS1 -Value \"command1\"\n" - expectedRemoveAliasLine := "Remove-Item Alias:ALIAS2\n" - - if !strings.Contains(output, expectedAliasLine) { - t.Errorf("PrintAlias() output missing expected line: %v", expectedAliasLine) - } - if !strings.Contains(output, expectedRemoveAliasLine) { - t.Errorf("PrintAlias() output missing expected line: %v", expectedRemoveAliasLine) - } - }) -} // TestDefaultShell_UnsetEnvs tests the UnsetEnvs method on Windows systems func TestDefaultShell_UnsetEnvs(t *testing.T) {