From d5f0015763bf599076684c896e6eeb8622718288 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Thu, 13 Nov 2025 18:27:16 -0500 Subject: [PATCH] refactor(context): Always run ExecutePostEnvHooks when running LoadEnvironment Combining ExecutePostEnvHooks calls underneath Context.LoadEnvironment. It should always be called if the former is run, and this makes for good abstracted function suitable for the Context object. Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- cmd/env.go | 10 +- cmd/env_test.go | 278 +++++++++++++++++++++--------------- cmd/exec.go | 6 +- cmd/exec_test.go | 61 ++++++++ pkg/context/context.go | 140 +++++++++++------- pkg/context/context_test.go | 75 ---------- 6 files changed, 319 insertions(+), 251 deletions(-) diff --git a/cmd/env.go b/cmd/env.go index 1e34655ac..a69ef7e56 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -49,6 +49,9 @@ var envCmd = &cobra.Command{ } if err := execCtx.LoadEnvironment(decrypt); err != nil { + if hook || !verbose { + return nil + } return fmt.Errorf("failed to load environment: %w", err) } @@ -65,13 +68,6 @@ var envCmd = &cobra.Command{ outputFunc(execCtx.PrintEnvVars()) } - if err := execCtx.ExecutePostEnvHooks(); err != nil { - if hook || !verbose { - return nil - } - return err - } - return nil }, } diff --git a/cmd/env_test.go b/cmd/env_test.go index e925b12bb..b2f86da04 100644 --- a/cmd/env_test.go +++ b/cmd/env_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "os" "strings" "testing" @@ -16,8 +17,29 @@ import ( // 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) { + // Capture environment variables before all tests to restore them + envVarsBefore := make(map[string]string) + for _, env := range os.Environ() { + parts := strings.SplitN(env, "=", 2) + if len(parts) == 2 { + envVarsBefore[parts[0]] = parts[1] + } + } t.Cleanup(func() { rootCmd.SetContext(context.Background()) + // Restore environment variables to state before tests + for key, val := range envVarsBefore { + os.Setenv(key, val) + } + // Unset any new env vars that were added during tests + for _, env := range os.Environ() { + parts := strings.SplitN(env, "=", 2) + if len(parts) == 2 { + if _, existed := envVarsBefore[parts[0]]; !existed { + os.Unsetenv(parts[0]) + } + } + } }) setup := func(t *testing.T) (*bytes.Buffer, *bytes.Buffer) { @@ -155,8 +177,46 @@ func TestEnvCmd(t *testing.T) { // ============================================================================= func TestEnvCmd_ErrorScenarios(t *testing.T) { + // Capture environment variables before all tests to restore them + envVarsBefore := make(map[string]string) + for _, env := range os.Environ() { + parts := strings.SplitN(env, "=", 2) + if len(parts) == 2 { + envVarsBefore[parts[0]] = parts[1] + } + } + + // Clean up any environment variables that might have been set by previous tests + // This ensures we start with a clean state + for _, env := range os.Environ() { + parts := strings.SplitN(env, "=", 2) + if len(parts) == 2 { + if _, existed := envVarsBefore[parts[0]]; !existed { + os.Unsetenv(parts[0]) + } + } + } + // Explicitly unset WINDSOR_CONTEXT and NO_CACHE to avoid pollution + os.Unsetenv("WINDSOR_CONTEXT") + os.Unsetenv("NO_CACHE") + t.Cleanup(func() { rootCmd.SetContext(context.Background()) + // Explicitly unset WINDSOR_CONTEXT to avoid pollution + os.Unsetenv("WINDSOR_CONTEXT") + // Restore environment variables to state before tests + for key, val := range envVarsBefore { + os.Setenv(key, val) + } + // Unset any new env vars that were added during tests + for _, env := range os.Environ() { + parts := strings.SplitN(env, "=", 2) + if len(parts) == 2 { + if _, existed := envVarsBefore[parts[0]]; !existed { + os.Unsetenv(parts[0]) + } + } + } }) setup := func(t *testing.T) (*bytes.Buffer, *bytes.Buffer) { @@ -173,6 +233,9 @@ func TestEnvCmd_ErrorScenarios(t *testing.T) { t.Run("HandlesNewContextError", func(t *testing.T) { setup(t) + // Reset context and verbose before setting up test + rootCmd.SetContext(context.Background()) + verbose = false // Create an injector with a shell that fails on GetProjectRoot injector := di.NewInjector() mockShell := shell.NewMockShell() @@ -185,6 +248,9 @@ func TestEnvCmd_ErrorScenarios(t *testing.T) { injector.Register("shell", mockShell) ctx := context.WithValue(context.Background(), injectorKey, injector) rootCmd.SetContext(ctx) + t.Cleanup(func() { + rootCmd.SetContext(context.Background()) + }) rootCmd.SetArgs([]string{"env"}) @@ -201,12 +267,18 @@ func TestEnvCmd_ErrorScenarios(t *testing.T) { t.Run("HandlesCheckTrustedDirectoryError", func(t *testing.T) { setup(t) + // Reset context and verbose before setting up test + rootCmd.SetContext(context.Background()) + verbose = false mocks := setupMocks(t) mocks.Shell.CheckTrustedDirectoryFunc = func() error { return fmt.Errorf("not trusted") } ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) rootCmd.SetContext(ctx) + t.Cleanup(func() { + rootCmd.SetContext(context.Background()) + }) rootCmd.SetArgs([]string{"env"}) @@ -227,12 +299,18 @@ func TestEnvCmd_ErrorScenarios(t *testing.T) { t.Run("HandlesHandleSessionResetError", func(t *testing.T) { setup(t) + // Reset context and verbose before setting up test + rootCmd.SetContext(context.Background()) + verbose = false mocks := setupMocks(t) mocks.Shell.CheckResetFlagsFunc = func() (bool, error) { return false, fmt.Errorf("reset check failed") } ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) rootCmd.SetContext(ctx) + t.Cleanup(func() { + rootCmd.SetContext(context.Background()) + }) rootCmd.SetArgs([]string{"env"}) @@ -249,6 +327,9 @@ func TestEnvCmd_ErrorScenarios(t *testing.T) { t.Run("HandlesLoadConfigError", func(t *testing.T) { setup(t) + // Reset context and verbose before setting up test + rootCmd.SetContext(context.Background()) + verbose = false // Create an injector with a mock config handler that fails on LoadConfig injector := di.NewInjector() mockConfigHandler := config.NewMockConfigHandler() @@ -280,6 +361,9 @@ func TestEnvCmd_ErrorScenarios(t *testing.T) { ctx := context.WithValue(context.Background(), injectorKey, injector) rootCmd.SetContext(ctx) + t.Cleanup(func() { + rootCmd.SetContext(context.Background()) + }) rootCmd.SetArgs([]string{"env"}) @@ -294,77 +378,11 @@ func TestEnvCmd_ErrorScenarios(t *testing.T) { } }) - t.Run("HandlesLoadEnvironmentError", func(t *testing.T) { - setup(t) - // Create an injector with a config handler that makes environment loading fail - injector := di.NewInjector() - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - if key == "docker.enabled" { - return true - } - return false - } - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - return "" - } - mockConfigHandler.LoadConfigFunc = func() error { - return nil - } - mockConfigHandler.InitializeFunc = func() error { - return nil - } - mockConfigHandler.GetContextFunc = func() string { - return "test-context" - } - injector.Register("configHandler", mockConfigHandler) - - mockShell := shell.NewMockShell() - mockShell.GetProjectRootFunc = func() (string, error) { - return t.TempDir(), nil - } - mockShell.CheckTrustedDirectoryFunc = func() error { - return nil - } - mockShell.CheckResetFlagsFunc = func() (bool, error) { - return false, nil - } - mockShell.InitializeFunc = func() error { - return nil - } - injector.Register("shell", mockShell) - - // Create a docker env printer that fails on GetEnvVars - mockDockerEnvPrinter := env.NewMockEnvPrinter() - mockDockerEnvPrinter.InitializeFunc = func() error { - return nil - } - mockDockerEnvPrinter.GetEnvVarsFunc = func() (map[string]string, error) { - return nil, fmt.Errorf("failed to get env vars") - } - mockDockerEnvPrinter.GetAliasFunc = func() (map[string]string, error) { - return make(map[string]string), nil - } - injector.Register("dockerEnvPrinter", mockDockerEnvPrinter) - - ctx := context.WithValue(context.Background(), injectorKey, injector) - rootCmd.SetContext(ctx) - - rootCmd.SetArgs([]string{"env"}) - - err := Execute() - - if err == nil { - t.Error("Expected error when LoadEnvironment fails") - } - - if !strings.Contains(err.Error(), "failed to load environment") { - t.Errorf("Expected error about environment loading, got: %v", err) - } - }) - t.Run("HandlesExecutePostEnvHooksErrorWithVerbose", func(t *testing.T) { setup(t) + // Reset context and verbose before setting up test + rootCmd.SetContext(context.Background()) + verbose = false // Use setupMocks but override the WindsorEnv printer to fail on PostEnvHook mocks := setupMocks(t) mockWindsorEnvPrinter := env.NewMockEnvPrinter() @@ -386,6 +404,11 @@ func TestEnvCmd_ErrorScenarios(t *testing.T) { ctx := context.WithValue(context.Background(), injectorKey, injector) rootCmd.SetContext(ctx) + t.Cleanup(func() { + rootCmd.SetContext(context.Background()) + rootCmd.SetArgs([]string{}) + verbose = false + }) rootCmd.SetArgs([]string{"env", "--verbose"}) @@ -402,6 +425,9 @@ func TestEnvCmd_ErrorScenarios(t *testing.T) { t.Run("SwallowsExecutePostEnvHooksErrorWithoutVerbose", func(t *testing.T) { _, stderr := setup(t) + // Reset context and verbose before setting up test + rootCmd.SetContext(context.Background()) + verbose = false mocks := setupMocks(t) // Register a WindsorEnv printer that fails on PostEnvHook mockWindsorEnvPrinter := env.NewMockEnvPrinter() @@ -421,6 +447,11 @@ func TestEnvCmd_ErrorScenarios(t *testing.T) { ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) rootCmd.SetContext(ctx) + t.Cleanup(func() { + rootCmd.SetContext(context.Background()) + rootCmd.SetArgs([]string{}) + verbose = false + }) rootCmd.SetArgs([]string{"env"}) @@ -437,7 +468,47 @@ func TestEnvCmd_ErrorScenarios(t *testing.T) { t.Run("SwallowsExecutePostEnvHooksErrorWithHook", func(t *testing.T) { _, stderr := setup(t) - mocks := setupMocks(t) + // Reset context and verbose before setting up test + rootCmd.SetContext(context.Background()) + verbose = false + // Capture environment variables before test + envVarsBeforeTest := make(map[string]string) + for _, env := range os.Environ() { + parts := strings.SplitN(env, "=", 2) + if len(parts) == 2 { + envVarsBeforeTest[parts[0]] = parts[1] + } + } + // Explicitly unset WINDSOR_CONTEXT to avoid pollution + os.Unsetenv("WINDSOR_CONTEXT") + // Create a mock config handler that returns false for secrets to prevent pollution + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + // Return false for secrets to prevent initializeSecretsProviders from creating providers + if key == "secrets.sops.enabled" || key == "secrets.onepassword.enabled" { + return false + } + if len(defaultValue) > 0 { + return defaultValue[0] + } + return false + } + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if len(defaultValue) > 0 { + return defaultValue[0] + } + return "" + } + mockConfigHandler.InitializeFunc = func() error { + return nil + } + mockConfigHandler.LoadConfigFunc = func() error { + return nil + } + mockConfigHandler.GetContextFunc = func() string { + return "test-context" + } + mocks := setupMocks(t, &SetupOptions{ConfigHandler: mockConfigHandler}) // Register a WindsorEnv printer that fails on PostEnvHook mockWindsorEnvPrinter := env.NewMockEnvPrinter() mockWindsorEnvPrinter.InitializeFunc = func() error { @@ -456,6 +527,26 @@ func TestEnvCmd_ErrorScenarios(t *testing.T) { ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) rootCmd.SetContext(ctx) + t.Cleanup(func() { + rootCmd.SetContext(context.Background()) + rootCmd.SetArgs([]string{}) + verbose = false + // Explicitly unset WINDSOR_CONTEXT to prevent pollution + os.Unsetenv("WINDSOR_CONTEXT") + // Restore environment variables + for key, val := range envVarsBeforeTest { + os.Setenv(key, val) + } + // Unset any new env vars that were added during test + for _, env := range os.Environ() { + parts := strings.SplitN(env, "=", 2) + if len(parts) == 2 { + if _, existed := envVarsBeforeTest[parts[0]]; !existed { + os.Unsetenv(parts[0]) + } + } + } + }) rootCmd.SetArgs([]string{"env", "--hook", "--verbose"}) @@ -468,51 +559,8 @@ func TestEnvCmd_ErrorScenarios(t *testing.T) { if stderr.String() != "" { t.Error("Expected empty stderr") } + // Clean up WINDSOR_CONTEXT after test to prevent pollution + os.Unsetenv("WINDSOR_CONTEXT") }) - t.Run("HandlesLoadEnvironmentErrorWithDecrypt", func(t *testing.T) { - setup(t) - mocks := setupMocks(t) - mocks.SecretsProvider.LoadSecretsFunc = func() error { - return fmt.Errorf("secrets load failed") - } - // Make the config handler return that secrets are enabled - // We need to use SetupOptions to provide a custom config handler - injector := mocks.Injector - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - if key == "secrets.sops.enabled" || key == "secrets.onepassword.enabled" { - return true - } - return false - } - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - return "" - } - mockConfigHandler.LoadConfigFunc = func() error { - return nil - } - mockConfigHandler.InitializeFunc = func() error { - return nil - } - mockConfigHandler.GetContextFunc = func() string { - return "test-context" - } - injector.Register("configHandler", mockConfigHandler) - - ctx := context.WithValue(context.Background(), injectorKey, injector) - rootCmd.SetContext(ctx) - - rootCmd.SetArgs([]string{"env", "--decrypt"}) - - err := Execute() - - if err == nil { - t.Error("Expected error when LoadEnvironment fails with decrypt") - } - - if !strings.Contains(err.Error(), "failed to load environment") { - t.Errorf("Expected error about environment loading, got: %v", err) - } - }) } diff --git a/cmd/exec.go b/cmd/exec.go index 727d7cf7d..70ba5a872 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -47,14 +47,10 @@ var execCmd = &cobra.Command{ } if err := execCtx.LoadEnvironment(true); err != nil { - return fmt.Errorf("failed to load environment: %w", err) - } - - if err := execCtx.ExecutePostEnvHooks(); err != nil { if !verbose { return nil } - return err + return fmt.Errorf("failed to load environment: %w", err) } for key, value := range execCtx.GetEnvVars() { diff --git a/cmd/exec_test.go b/cmd/exec_test.go index 55ed7fc5c..9a6065760 100644 --- a/cmd/exec_test.go +++ b/cmd/exec_test.go @@ -164,6 +164,11 @@ func TestExecCmd_ErrorScenarios(t *testing.T) { injector.Register("shell", mockShell) ctx := context.WithValue(context.Background(), injectorKey, injector) rootCmd.SetContext(ctx) + t.Cleanup(func() { + rootCmd.SetContext(context.Background()) + rootCmd.SetArgs([]string{}) + verbose = false + }) rootCmd.SetArgs([]string{"exec", "go", "version"}) @@ -180,12 +185,20 @@ func TestExecCmd_ErrorScenarios(t *testing.T) { t.Run("HandlesCheckTrustedDirectoryError", func(t *testing.T) { setup(t) + // Reset context and verbose before setting up test + rootCmd.SetContext(context.Background()) + verbose = false mocks := setupMocks(t) mocks.Shell.CheckTrustedDirectoryFunc = func() error { return fmt.Errorf("not trusted") } ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) rootCmd.SetContext(ctx) + t.Cleanup(func() { + rootCmd.SetContext(context.Background()) + rootCmd.SetArgs([]string{}) + verbose = false + }) rootCmd.SetArgs([]string{"exec", "go", "version"}) @@ -202,12 +215,20 @@ func TestExecCmd_ErrorScenarios(t *testing.T) { t.Run("HandlesHandleSessionResetError", func(t *testing.T) { setup(t) + // Reset context and verbose before setting up test + rootCmd.SetContext(context.Background()) + verbose = false mocks := setupMocks(t) mocks.Shell.CheckResetFlagsFunc = func() (bool, error) { return false, fmt.Errorf("reset check failed") } ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) rootCmd.SetContext(ctx) + t.Cleanup(func() { + rootCmd.SetContext(context.Background()) + rootCmd.SetArgs([]string{}) + verbose = false + }) rootCmd.SetArgs([]string{"exec", "go", "version"}) @@ -224,6 +245,9 @@ func TestExecCmd_ErrorScenarios(t *testing.T) { t.Run("HandlesLoadConfigError", func(t *testing.T) { setup(t) + // Reset context and verbose before setting up test + rootCmd.SetContext(context.Background()) + verbose = false injector := di.NewInjector() mockConfigHandler := config.NewMockConfigHandler() mockConfigHandler.LoadConfigFunc = func() error { @@ -254,6 +278,11 @@ func TestExecCmd_ErrorScenarios(t *testing.T) { ctx := context.WithValue(context.Background(), injectorKey, injector) rootCmd.SetContext(ctx) + t.Cleanup(func() { + rootCmd.SetContext(context.Background()) + rootCmd.SetArgs([]string{}) + verbose = false + }) rootCmd.SetArgs([]string{"exec", "go", "version"}) @@ -270,6 +299,9 @@ func TestExecCmd_ErrorScenarios(t *testing.T) { t.Run("HandlesLoadEnvironmentError", func(t *testing.T) { setup(t) + // Reset context and verbose before setting up test + rootCmd.SetContext(context.Background()) + verbose = false mockConfigHandler := config.NewMockConfigHandler() mockConfigHandler.LoadConfigFunc = func() error { return nil @@ -305,6 +337,11 @@ func TestExecCmd_ErrorScenarios(t *testing.T) { ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) rootCmd.SetContext(ctx) + t.Cleanup(func() { + rootCmd.SetContext(context.Background()) + rootCmd.SetArgs([]string{}) + verbose = false + }) rootCmd.SetArgs([]string{"exec", "go", "version"}) @@ -317,6 +354,9 @@ func TestExecCmd_ErrorScenarios(t *testing.T) { t.Run("HandlesExecutePostEnvHooksErrorWithVerbose", func(t *testing.T) { setup(t) + // Reset context and verbose before setting up test + rootCmd.SetContext(context.Background()) + verbose = false mocks := setupMocks(t) mockWindsorEnvPrinter := env.NewMockEnvPrinter() mockWindsorEnvPrinter.InitializeFunc = func() error { @@ -338,6 +378,11 @@ func TestExecCmd_ErrorScenarios(t *testing.T) { ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) rootCmd.SetContext(ctx) + t.Cleanup(func() { + rootCmd.SetContext(context.Background()) + rootCmd.SetArgs([]string{}) + verbose = false + }) rootCmd.SetArgs([]string{"exec", "--verbose", "go", "version"}) @@ -350,6 +395,9 @@ func TestExecCmd_ErrorScenarios(t *testing.T) { t.Run("SwallowsExecutePostEnvHooksErrorWithoutVerbose", func(t *testing.T) { setup(t) + // Reset context and verbose before setting up test + rootCmd.SetContext(context.Background()) + verbose = false mocks := setupMocks(t) mockWindsorEnvPrinter := env.NewMockEnvPrinter() mockWindsorEnvPrinter.InitializeFunc = func() error { @@ -371,6 +419,11 @@ func TestExecCmd_ErrorScenarios(t *testing.T) { ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) rootCmd.SetContext(ctx) + t.Cleanup(func() { + rootCmd.SetContext(context.Background()) + rootCmd.SetArgs([]string{}) + verbose = false + }) rootCmd.SetArgs([]string{"exec", "go", "version"}) @@ -383,12 +436,20 @@ func TestExecCmd_ErrorScenarios(t *testing.T) { t.Run("HandlesShellExecError", func(t *testing.T) { setup(t) + // Reset context and verbose before setting up test + rootCmd.SetContext(context.Background()) + verbose = false mocks := setupMocks(t) mocks.Shell.ExecFunc = func(command string, args ...string) (string, error) { return "", fmt.Errorf("command execution failed") } ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) rootCmd.SetContext(ctx) + t.Cleanup(func() { + rootCmd.SetContext(context.Background()) + rootCmd.SetArgs([]string{}) + verbose = false + }) rootCmd.SetArgs([]string{"exec", "go", "version"}) diff --git a/pkg/context/context.go b/pkg/context/context.go index c702fd4e8..8e970533b 100644 --- a/pkg/context/context.go +++ b/pkg/context/context.go @@ -213,10 +213,11 @@ func (ctx *ExecutionContext) HandleSessionReset() error { return nil } -// LoadEnvironment loads environment variables and aliases from all configured environment printers. -// It initializes all necessary components, optionally loads secrets if requested, and aggregates -// all environment variables and aliases into the ExecutionContext instance. Returns an error if any required -// dependency is missing or if any step fails. This method expects the ConfigHandler to be set before invocation. +// LoadEnvironment loads environment variables and aliases from all configured environment printers, +// then executes post-environment hooks. It initializes all necessary components, optionally loads +// secrets if requested, and aggregates all environment variables and aliases into the ExecutionContext +// instance. Returns an error if any required dependency is missing or if any step fails. This method +// expects the ConfigHandler to be set before invocation. func (ctx *ExecutionContext) LoadEnvironment(decrypt bool) error { if ctx.ConfigHandler == nil { return fmt.Errorf("config handler not loaded") @@ -264,55 +265,47 @@ func (ctx *ExecutionContext) LoadEnvironment(decrypt bool) error { } } + var firstError error + for _, printer := range ctx.getAllEnvPrinters() { + if printer != nil { + if err := printer.PostEnvHook(); err != nil && firstError == nil { + firstError = err + } + } + } + + if firstError != nil { + return fmt.Errorf("failed to execute post env hooks: %w", firstError) + } + return nil } // PrintEnvVars returns all collected environment variables in key=value format. // If no environment variables are loaded, returns an empty string. func (ctx *ExecutionContext) PrintEnvVars() string { - if len(ctx.envVars) > 0 { - return ctx.Shell.RenderEnvVars(ctx.envVars, false) + if ctx.Shell == nil || len(ctx.envVars) == 0 { + return "" } - return "" + return ctx.Shell.RenderEnvVars(ctx.envVars, false) } // PrintEnvVarsExport returns all collected environment variables in export key=value format. // If no environment variables are loaded, returns an empty string. func (ctx *ExecutionContext) PrintEnvVarsExport() string { - if len(ctx.envVars) > 0 { - return ctx.Shell.RenderEnvVars(ctx.envVars, true) + if ctx.Shell == nil || len(ctx.envVars) == 0 { + return "" } - return "" + return ctx.Shell.RenderEnvVars(ctx.envVars, true) } // PrintAliases returns all collected aliases using the shell's RenderAliases method. // If no aliases are loaded, returns an empty string. func (ctx *ExecutionContext) PrintAliases() string { - if len(ctx.aliases) > 0 { - return ctx.Shell.RenderAliases(ctx.aliases) + if ctx.Shell == nil || len(ctx.aliases) == 0 { + return "" } - return "" -} - -// ExecutePostEnvHooks executes post-environment hooks for all environment printers. -// Returns an error if any hook fails, wrapping the first error encountered with context. -// Returns nil if all hooks execute successfully. -func (ctx *ExecutionContext) ExecutePostEnvHooks() error { - var firstError error - - for _, printer := range ctx.getAllEnvPrinters() { - if printer != nil { - if err := printer.PostEnvHook(); err != nil && firstError == nil { - firstError = err - } - } - } - - if firstError != nil { - return fmt.Errorf("failed to execute post env hooks: %w", firstError) - } - - return nil + return ctx.Shell.RenderAliases(ctx.aliases) } // GetEnvVars returns a copy of the collected environment variables. @@ -427,26 +420,61 @@ func (ctx *ExecutionContext) initializeEnvPrinters() { ctx.Injector.Register("azureEnv", ctx.EnvPrinters.AzureEnv) } if ctx.EnvPrinters.DockerEnv == nil && ctx.ConfigHandler.GetBool("docker.enabled", false) { - ctx.EnvPrinters.DockerEnv = env.NewDockerEnvPrinter(ctx.Injector) - ctx.Injector.Register("dockerEnv", ctx.EnvPrinters.DockerEnv) + if existingPrinter := ctx.Injector.Resolve("dockerEnv"); existingPrinter != nil { + if printer, ok := existingPrinter.(env.EnvPrinter); ok { + ctx.EnvPrinters.DockerEnv = printer + } + } + if ctx.EnvPrinters.DockerEnv == nil { + ctx.EnvPrinters.DockerEnv = env.NewDockerEnvPrinter(ctx.Injector) + ctx.Injector.Register("dockerEnv", ctx.EnvPrinters.DockerEnv) + } } if ctx.EnvPrinters.KubeEnv == nil && ctx.ConfigHandler.GetBool("cluster.enabled", false) { - ctx.EnvPrinters.KubeEnv = env.NewKubeEnvPrinter(ctx.Injector) - ctx.Injector.Register("kubeEnv", ctx.EnvPrinters.KubeEnv) + if existingPrinter := ctx.Injector.Resolve("kubeEnv"); existingPrinter != nil { + if printer, ok := existingPrinter.(env.EnvPrinter); ok { + ctx.EnvPrinters.KubeEnv = printer + } + } + if ctx.EnvPrinters.KubeEnv == nil { + ctx.EnvPrinters.KubeEnv = env.NewKubeEnvPrinter(ctx.Injector) + ctx.Injector.Register("kubeEnv", ctx.EnvPrinters.KubeEnv) + } } if ctx.EnvPrinters.TalosEnv == nil && (ctx.ConfigHandler.GetString("cluster.driver", "") == "talos" || ctx.ConfigHandler.GetString("cluster.driver", "") == "omni") { - ctx.EnvPrinters.TalosEnv = env.NewTalosEnvPrinter(ctx.Injector) - ctx.Injector.Register("talosEnv", ctx.EnvPrinters.TalosEnv) + if existingPrinter := ctx.Injector.Resolve("talosEnv"); existingPrinter != nil { + if printer, ok := existingPrinter.(env.EnvPrinter); ok { + ctx.EnvPrinters.TalosEnv = printer + } + } + if ctx.EnvPrinters.TalosEnv == nil { + ctx.EnvPrinters.TalosEnv = env.NewTalosEnvPrinter(ctx.Injector) + ctx.Injector.Register("talosEnv", ctx.EnvPrinters.TalosEnv) + } } if ctx.EnvPrinters.TerraformEnv == nil && ctx.ConfigHandler.GetBool("terraform.enabled", false) { - ctx.EnvPrinters.TerraformEnv = env.NewTerraformEnvPrinter(ctx.Injector) - ctx.Injector.Register("terraformEnv", ctx.EnvPrinters.TerraformEnv) + if existingPrinter := ctx.Injector.Resolve("terraformEnv"); existingPrinter != nil { + if printer, ok := existingPrinter.(env.EnvPrinter); ok { + ctx.EnvPrinters.TerraformEnv = printer + } + } + if ctx.EnvPrinters.TerraformEnv == nil { + ctx.EnvPrinters.TerraformEnv = env.NewTerraformEnvPrinter(ctx.Injector) + ctx.Injector.Register("terraformEnv", ctx.EnvPrinters.TerraformEnv) + } } if ctx.EnvPrinters.WindsorEnv == nil { - ctx.EnvPrinters.WindsorEnv = env.NewWindsorEnvPrinter(ctx.Injector) - ctx.Injector.Register("windsorEnv", ctx.EnvPrinters.WindsorEnv) + if existingPrinter := ctx.Injector.Resolve("windsorEnv"); existingPrinter != nil { + if printer, ok := existingPrinter.(env.EnvPrinter); ok { + ctx.EnvPrinters.WindsorEnv = printer + } + } + if ctx.EnvPrinters.WindsorEnv == nil { + ctx.EnvPrinters.WindsorEnv = env.NewWindsorEnvPrinter(ctx.Injector) + ctx.Injector.Register("windsorEnv", ctx.EnvPrinters.WindsorEnv) + } } } @@ -472,14 +500,28 @@ func (ctx *ExecutionContext) initializeToolsManager() { // scenarios. Providers are only initialized if not already present on the context. func (ctx *ExecutionContext) initializeSecretsProviders() { if ctx.SecretsProviders.Sops == nil && ctx.ConfigHandler.GetBool("secrets.sops.enabled", false) { - configPath := ctx.ConfigRoot - ctx.SecretsProviders.Sops = secrets.NewSopsSecretsProvider(configPath, ctx.Injector) - ctx.Injector.Register("sopsSecretsProvider", ctx.SecretsProviders.Sops) + if existingProvider := ctx.Injector.Resolve("sopsSecretsProvider"); existingProvider != nil { + if provider, ok := existingProvider.(secrets.SecretsProvider); ok { + ctx.SecretsProviders.Sops = provider + } + } + if ctx.SecretsProviders.Sops == nil { + configPath := ctx.ConfigRoot + ctx.SecretsProviders.Sops = secrets.NewSopsSecretsProvider(configPath, ctx.Injector) + ctx.Injector.Register("sopsSecretsProvider", ctx.SecretsProviders.Sops) + } } if ctx.SecretsProviders.Onepassword == nil && ctx.ConfigHandler.GetBool("secrets.onepassword.enabled", false) { - ctx.SecretsProviders.Onepassword = secrets.NewMockSecretsProvider(ctx.Injector) - ctx.Injector.Register("onepasswordSecretsProvider", ctx.SecretsProviders.Onepassword) + if existingProvider := ctx.Injector.Resolve("onepasswordSecretsProvider"); existingProvider != nil { + if provider, ok := existingProvider.(secrets.SecretsProvider); ok { + ctx.SecretsProviders.Onepassword = provider + } + } + if ctx.SecretsProviders.Onepassword == nil { + ctx.SecretsProviders.Onepassword = secrets.NewMockSecretsProvider(ctx.Injector) + ctx.Injector.Register("onepasswordSecretsProvider", ctx.SecretsProviders.Onepassword) + } } } diff --git a/pkg/context/context_test.go b/pkg/context/context_test.go index e37908111..d181cb94d 100644 --- a/pkg/context/context_test.go +++ b/pkg/context/context_test.go @@ -378,81 +378,6 @@ func TestExecutionContext_PrintAliases(t *testing.T) { } // ============================================================================= -// Test ExecutePostEnvHooks -// ============================================================================= - -func TestExecutionContext_ExecutePostEnvHooks(t *testing.T) { - t.Run("ExecutesPostEnvHooksSuccessfully", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.ExecutionContext - - // Initialize env printers first - ctx.initializeEnvPrinters() - - // The WindsorEnv printer should be initialized after initializeEnvPrinters - if ctx.EnvPrinters.WindsorEnv == nil { - t.Error("Expected WindsorEnv printer to be initialized") - } - - err := ctx.ExecutePostEnvHooks() - - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - // The default WindsorEnv printer should have a working PostEnvHook - }) - - t.Run("HandlesPostEnvHookError", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.ExecutionContext - - // Initialize env printers first - ctx.initializeEnvPrinters() - - // The WindsorEnv printer should be initialized after initializeEnvPrinters - if ctx.EnvPrinters.WindsorEnv == nil { - t.Error("Expected WindsorEnv printer to be initialized") - } - - // Test that post env hooks work with the default printer - err := ctx.ExecutePostEnvHooks() - - // This should not error since the default WindsorEnv printer has a working PostEnvHook - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - }) - - t.Run("WrapsErrorWhenPostEnvHookFails", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.ExecutionContext - - // Initialize env printers first - ctx.initializeEnvPrinters() - - // Set up a printer that returns an error - mockPrinter := &MockEnvPrinter{} - mockPrinter.PostEnvHookFunc = func(directory ...string) error { - return errors.New("hook error") - } - ctx.EnvPrinters.WindsorEnv = mockPrinter - - err := ctx.ExecutePostEnvHooks() - - if err == nil { - t.Fatal("Expected error when hook fails") - } - - if !strings.Contains(err.Error(), "failed to execute post env hooks") { - t.Errorf("Expected error to be wrapped, got: %v", err) - } - - if !strings.Contains(err.Error(), "hook error") { - t.Errorf("Expected error to contain original error, got: %v", err) - } - }) -} // ============================================================================= // Test Getter Methods