From 728813b025f5f16683117708a847e4b6540fdf9c Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:15:54 -0500 Subject: [PATCH 1/2] refactor(cmd): Migrate check to use context mechanism Migrates `windsor check` to leverage the new context (runtime) mechanism. Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- cmd/check.go | 96 +++-- cmd/check_test.go | 529 ++++++++++++++++++++++++---- pkg/context/context.go | 29 ++ pkg/context/context_test.go | 106 ++++++ pkg/provisioner/provisioner.go | 130 ++++++- pkg/provisioner/provisioner_test.go | 394 +++++++++++++++++++++ 6 files changed, 1172 insertions(+), 112 deletions(-) diff --git a/cmd/check.go b/cmd/check.go index 16399d4ab..a5332ae92 100644 --- a/cmd/check.go +++ b/cmd/check.go @@ -1,14 +1,14 @@ package cmd import ( - "context" "fmt" "time" "github.com/spf13/cobra" "github.com/windsorcli/cli/pkg/constants" + "github.com/windsorcli/cli/pkg/context" "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/pipelines" + "github.com/windsorcli/cli/pkg/provisioner" ) var ( @@ -25,27 +25,31 @@ var checkCmd = &cobra.Command{ Long: "Check the tool versions required by the project", SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - // Get shared dependency injector from context injector := cmd.Context().Value(injectorKey).(di.Injector) - // Create output function - outputFunc := func(output string) { - fmt.Fprintln(cmd.OutOrStdout(), output) + execCtx := &context.ExecutionContext{ + Injector: injector, } - // Create execution context with operation and output function - ctx := context.WithValue(cmd.Context(), "operation", "tools") - ctx = context.WithValue(ctx, "output", outputFunc) - - // Set up the check pipeline - pipeline, err := pipelines.WithPipeline(injector, ctx, "checkPipeline") + execCtx, err := context.NewContext(execCtx) if err != nil { - return fmt.Errorf("failed to set up check pipeline: %w", err) + return fmt.Errorf("failed to initialize context: %w", err) + } + + if err := execCtx.CheckTrustedDirectory(); err != nil { + return fmt.Errorf("not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve") + } + + if err := execCtx.LoadConfig(); err != nil { + return err } - // Execute the pipeline - if err := pipeline.Execute(ctx); err != nil { - return fmt.Errorf("Error executing check pipeline: %w", err) + if !execCtx.ConfigHandler.IsLoaded() { + return fmt.Errorf("Nothing to check. Have you run \033[1mwindsor init\033[0m?") + } + + if err := execCtx.CheckTools(); err != nil { + return err } return nil @@ -58,43 +62,63 @@ var checkNodeHealthCmd = &cobra.Command{ Long: "Check the health status of specified cluster nodes", SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - // Get shared dependency injector from context injector := cmd.Context().Value(injectorKey).(di.Injector) - // Require at least one health check type to be specified if len(nodeHealthNodes) == 0 && k8sEndpoint == "" { return fmt.Errorf("No health checks specified. Use --nodes and/or --k8s-endpoint flags to specify health checks to perform") } - // If timeout is not set via flag, use default if !cmd.Flags().Changed("timeout") { nodeHealthTimeout = constants.DefaultNodeHealthCheckTimeout } - // Create output function + execCtx := &context.ExecutionContext{ + Injector: injector, + } + + execCtx, err := context.NewContext(execCtx) + if err != nil { + return fmt.Errorf("failed to initialize context: %w", err) + } + + if err := execCtx.CheckTrustedDirectory(); err != nil { + return fmt.Errorf("not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve") + } + + if err := execCtx.LoadConfig(); err != nil { + return err + } + + if !execCtx.ConfigHandler.IsLoaded() { + return fmt.Errorf("Nothing to check. Have you run \033[1mwindsor init\033[0m?") + } + + provisionerCtx := &provisioner.ProvisionerExecutionContext{ + ExecutionContext: *execCtx, + } + + prov := provisioner.NewProvisioner(provisionerCtx) + outputFunc := func(output string) { fmt.Fprintln(cmd.OutOrStdout(), output) } - // Create execution context with operation, nodes, timeout, version, and output function - ctx := context.WithValue(cmd.Context(), "operation", "node-health") - ctx = context.WithValue(ctx, "nodes", nodeHealthNodes) - ctx = context.WithValue(ctx, "timeout", nodeHealthTimeout) - ctx = context.WithValue(ctx, "version", nodeHealthVersion) - ctx = context.WithValue(ctx, "k8s-endpoint", k8sEndpoint) - ctx = context.WithValue(ctx, "k8s-endpoint-provided", k8sEndpoint != "" || checkNodeReady) - ctx = context.WithValue(ctx, "check-node-ready", checkNodeReady) - ctx = context.WithValue(ctx, "output", outputFunc) + k8sEndpointStr := k8sEndpoint + if k8sEndpointStr == "" && checkNodeReady { + k8sEndpointStr = "true" + } - // Set up the check pipeline - pipeline, err := pipelines.WithPipeline(injector, ctx, "checkPipeline") - if err != nil { - return fmt.Errorf("failed to set up check pipeline: %w", err) + options := provisioner.NodeHealthCheckOptions{ + Nodes: nodeHealthNodes, + Timeout: nodeHealthTimeout, + Version: nodeHealthVersion, + K8SEndpoint: k8sEndpointStr, + K8SEndpointProvided: k8sEndpoint != "" || checkNodeReady, + CheckNodeReady: checkNodeReady, } - // Execute the pipeline - if err := pipeline.Execute(ctx); err != nil { - return fmt.Errorf("Error executing check pipeline: %w", err) + if err := prov.CheckNodeHealth(cmd.Context(), options, outputFunc); err != nil { + return fmt.Errorf("error checking node health: %w", err) } return nil diff --git a/cmd/check_test.go b/cmd/check_test.go index 625d90f51..4b8b3f414 100644 --- a/cmd/check_test.go +++ b/cmd/check_test.go @@ -2,10 +2,18 @@ package cmd import ( "bytes" - "context" + stdcontext "context" + "fmt" "os" "strings" "testing" + + "github.com/windsorcli/cli/pkg/context/config" + "github.com/windsorcli/cli/pkg/context/shell" + "github.com/windsorcli/cli/pkg/context/tools" + "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/provisioner/cluster" + "github.com/windsorcli/cli/pkg/provisioner/kubernetes" ) // Helper function to check if a string contains a substring @@ -15,7 +23,7 @@ func checkContains(str, substr string) bool { func TestCheckCmd(t *testing.T) { t.Cleanup(func() { - rootCmd.SetContext(context.Background()) + rootCmd.SetContext(stdcontext.Background()) }) setup := func(t *testing.T, withConfig bool) (*bytes.Buffer, *bytes.Buffer) { @@ -60,10 +68,35 @@ func TestCheckCmd(t *testing.T) { t.Run("Success", func(t *testing.T) { // Given a directory with proper configuration - stdout, stderr := setup(t, true) + setup(t, true) + + // Set up mocks with trusted directory + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.LoadConfigFunc = func() error { + return nil + } + mockConfigHandler.InitializeFunc = func() error { + return nil + } + mockConfigHandler.GetContextFunc = func() string { + return "test-context" + } + mockConfigHandler.IsLoadedFunc = func() bool { + return true + } + mocks := setupMocks(t, &SetupOptions{ConfigHandler: mockConfigHandler}) - // Reset context for fresh test - rootCmd.SetContext(context.Background()) + ctx := stdcontext.WithValue(stdcontext.Background(), injectorKey, mocks.Injector) + rootCmd.SetContext(ctx) + + mockToolsManager := tools.NewMockToolsManager() + mockToolsManager.InitializeFunc = func() error { + return nil + } + mockToolsManager.CheckFunc = func() error { + return nil + } + mocks.Injector.Register("toolsManager", mockToolsManager) // When executing the command err := Execute() @@ -72,23 +105,30 @@ func TestCheckCmd(t *testing.T) { if err != nil { t.Errorf("Expected success, got error: %v", err) } - - // And output should contain success message - output := stdout.String() - if output != "All tools are up to date.\n" { - t.Errorf("Expected 'All tools are up to date.', got: %q", output) - } - if stderr.String() != "" { - t.Error("Expected empty stderr") - } }) t.Run("ConfigNotLoaded", func(t *testing.T) { // Given a directory with no configuration _, _ = setup(t, false) - // Reset context for fresh test - rootCmd.SetContext(context.Background()) + // Set up mocks with trusted directory but no config loaded + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.LoadConfigFunc = func() error { + return nil + } + mockConfigHandler.InitializeFunc = func() error { + return nil + } + mockConfigHandler.GetContextFunc = func() string { + return "test-context" + } + mockConfigHandler.IsLoadedFunc = func() bool { + return false + } + mocks := setupMocks(t, &SetupOptions{ConfigHandler: mockConfigHandler}) + + ctx := stdcontext.WithValue(stdcontext.Background(), injectorKey, mocks.Injector) + rootCmd.SetContext(ctx) // When executing the command err := Execute() @@ -99,17 +139,169 @@ func TestCheckCmd(t *testing.T) { } // And error should contain init message - expectedError := "Error executing check pipeline: Nothing to check. Have you run \033[1mwindsor init\033[0m?" - if err.Error() != expectedError { + if !strings.Contains(err.Error(), "Nothing to check") { t.Errorf("Expected error about init, got: %v", err) } }) } +// ============================================================================= +// Test Error Scenarios +// ============================================================================= + +func TestCheckCmd_ErrorScenarios(t *testing.T) { + t.Cleanup(func() { + rootCmd.SetContext(stdcontext.Background()) + }) + + setup := func(t *testing.T) (*bytes.Buffer, *bytes.Buffer) { + t.Helper() + stdout, stderr := captureOutput(t) + rootCmd.SetOut(stdout) + rootCmd.SetErr(stderr) + return stdout, stderr + } + + t.Run("HandlesNewContextError", func(t *testing.T) { + setup(t) + injector := di.NewInjector() + mockShell := shell.NewMockShell() + mockShell.GetProjectRootFunc = func() (string, error) { + return "", fmt.Errorf("project root error") + } + mockShell.InitializeFunc = func() error { + return nil + } + injector.Register("shell", mockShell) + + ctx := stdcontext.WithValue(stdcontext.Background(), injectorKey, injector) + rootCmd.SetContext(ctx) + + rootCmd.SetArgs([]string{"check"}) + + err := Execute() + + if err == nil { + t.Error("Expected error when NewContext fails") + } + + if !strings.Contains(err.Error(), "failed to initialize context") { + t.Errorf("Expected error about context initialization, got: %v", err) + } + }) + + t.Run("HandlesCheckTrustedDirectoryError", func(t *testing.T) { + setup(t) + mocks := setupMocks(t) + mocks.Shell.CheckTrustedDirectoryFunc = func() error { + return fmt.Errorf("not trusted") + } + + ctx := stdcontext.WithValue(stdcontext.Background(), injectorKey, mocks.Injector) + rootCmd.SetContext(ctx) + + rootCmd.SetArgs([]string{"check"}) + + err := Execute() + + if err == nil { + t.Error("Expected error when CheckTrustedDirectory fails") + } + + if !strings.Contains(err.Error(), "not in a trusted directory") { + t.Errorf("Expected error about trusted directory, got: %v", err) + } + }) + + t.Run("HandlesLoadConfigError", func(t *testing.T) { + setup(t) + injector := di.NewInjector() + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.LoadConfigFunc = func() error { + return fmt.Errorf("config load failed") + } + 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.InitializeFunc = func() error { + return nil + } + injector.Register("shell", mockShell) + + ctx := stdcontext.WithValue(stdcontext.Background(), injectorKey, injector) + rootCmd.SetContext(ctx) + + rootCmd.SetArgs([]string{"check"}) + + err := Execute() + + if err == nil { + t.Error("Expected error when LoadConfig fails") + } + + if !strings.Contains(err.Error(), "config load failed") && !strings.Contains(err.Error(), "failed to load config") { + t.Errorf("Expected error about config loading, got: %v", err) + } + }) + + t.Run("HandlesCheckToolsError", func(t *testing.T) { + setup(t) + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.LoadConfigFunc = func() error { + return nil + } + mockConfigHandler.InitializeFunc = func() error { + return nil + } + mockConfigHandler.GetContextFunc = func() string { + return "test-context" + } + mockConfigHandler.IsLoadedFunc = func() bool { + return true + } + mocks := setupMocks(t, &SetupOptions{ConfigHandler: mockConfigHandler}) + mockToolsManager := tools.NewMockToolsManager() + mockToolsManager.InitializeFunc = func() error { + return nil + } + mockToolsManager.CheckFunc = func() error { + return fmt.Errorf("tools check failed") + } + mocks.Injector.Register("toolsManager", mockToolsManager) + + ctx := stdcontext.WithValue(stdcontext.Background(), injectorKey, mocks.Injector) + rootCmd.SetContext(ctx) + + rootCmd.SetArgs([]string{"check"}) + + err := Execute() + + if err == nil { + t.Error("Expected error when CheckTools fails") + } + + if !strings.Contains(err.Error(), "tools check failed") && !strings.Contains(err.Error(), "error checking tools") { + t.Errorf("Expected error about tools check, got: %v", err) + } + }) +} + func TestCheckNodeHealthCmd(t *testing.T) { // Cleanup: reset rootCmd context after all subtests complete t.Cleanup(func() { - rootCmd.SetContext(context.Background()) + rootCmd.SetContext(stdcontext.Background()) }) setup := func(t *testing.T, withConfig bool) (*bytes.Buffer, *bytes.Buffer) { @@ -166,131 +358,320 @@ func TestCheckNodeHealthCmd(t *testing.T) { // Given a directory with proper configuration _, _ = setup(t, true) + // Set up mocks + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.LoadConfigFunc = func() error { + return nil + } + mockConfigHandler.InitializeFunc = func() error { + return nil + } + mockConfigHandler.GetContextFunc = func() string { + return "test-context" + } + mockConfigHandler.IsLoadedFunc = func() bool { + return true + } + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "cluster.driver" { + return "talos" + } + return "" + } + mocks := setupMocks(t, &SetupOptions{ConfigHandler: mockConfigHandler}) + + mockClusterClient := cluster.NewMockClusterClient() + mockClusterClient.WaitForNodesHealthyFunc = func(ctx stdcontext.Context, nodeAddresses []string, expectedVersion string) error { + return fmt.Errorf("cluster health check failed") + } + mocks.Injector.Register("clusterClient", mockClusterClient) + + ctx := stdcontext.WithValue(stdcontext.Background(), injectorKey, mocks.Injector) + rootCmd.SetContext(ctx) + // Setup command args with nodes rootCmd.SetArgs([]string{"check", "node-health", "--nodes", "10.0.0.1,10.0.0.2"}) // When executing the command err := Execute() - // Then an error should occur (because cluster client can't be initialized without proper config) + // Then an error should occur if err == nil { t.Error("Expected error, got nil") } - // And error should contain cluster client message - if !checkContains(err.Error(), "Error executing check pipeline") { - t.Errorf("Expected error about pipeline execution, got: %v", err) + // And error should contain node health check message + if !strings.Contains(err.Error(), "error checking node health") && !strings.Contains(err.Error(), "nodes failed health check") { + t.Errorf("Expected error about node health check, got: %v", err) } }) - t.Run("ClusterClientErrorWithVersion", func(t *testing.T) { + t.Run("NoNodesSpecified", func(t *testing.T) { // Given a directory with proper configuration _, _ = setup(t, true) - // Setup command args with nodes and version - rootCmd.SetArgs([]string{"check", "node-health", "--nodes", "10.0.0.1", "--version", "1.0.0"}) + // Setup command args without nodes + rootCmd.SetArgs([]string{"check", "node-health"}) // When executing the command err := Execute() - // Then an error should occur (because cluster client can't be initialized without proper config) + // Then an error should occur if err == nil { t.Error("Expected error, got nil") } - // And error should contain cluster client message - if !checkContains(err.Error(), "Error executing check pipeline") { - t.Errorf("Expected error about pipeline execution, got: %v", err) + // And error should contain health checks message + expectedError := "No health checks specified. Use --nodes and/or --k8s-endpoint flags to specify health checks to perform" + if err.Error() != expectedError { + t.Errorf("Expected error about health checks, got: %v", err) } }) - t.Run("ClusterClientErrorWithTimeout", func(t *testing.T) { + t.Run("EmptyNodesFlag", func(t *testing.T) { // Given a directory with proper configuration _, _ = setup(t, true) - // Setup command args with nodes and timeout - rootCmd.SetArgs([]string{"check", "node-health", "--nodes", "10.0.0.1", "--timeout", "10s"}) + // Setup command args with empty nodes flag + rootCmd.SetArgs([]string{"check", "node-health", "--nodes", ""}) // When executing the command err := Execute() - // Then an error should occur (because cluster client can't be initialized without proper config) + // Then an error should occur if err == nil { t.Error("Expected error, got nil") } - // And error should contain cluster client message - if !checkContains(err.Error(), "Error executing check pipeline") { - t.Errorf("Expected error about pipeline execution, got: %v", err) + // And error should contain health checks message + expectedError := "No health checks specified. Use --nodes and/or --k8s-endpoint flags to specify health checks to perform" + if err.Error() != expectedError { + t.Errorf("Expected error about health checks, got: %v", err) } }) +} - t.Run("ConfigNotLoaded", func(t *testing.T) { - // Given a directory with no configuration - _, _ = setup(t, false) +// ============================================================================= +// Test Error Scenarios +// ============================================================================= + +func TestCheckNodeHealthCmd_ErrorScenarios(t *testing.T) { + t.Cleanup(func() { + rootCmd.SetContext(stdcontext.Background()) + nodeHealthTimeout = 0 + nodeHealthNodes = []string{} + nodeHealthVersion = "" + k8sEndpoint = "" + checkNodeReady = false + }) + + setup := func(t *testing.T) (*bytes.Buffer, *bytes.Buffer) { + t.Helper() + stdout, stderr := captureOutput(t) + rootCmd.SetOut(stdout) + rootCmd.SetErr(stderr) - // Reset context for fresh test - rootCmd.SetContext(context.Background()) + nodeHealthTimeout = 0 + nodeHealthNodes = []string{} + nodeHealthVersion = "" + k8sEndpoint = "" + checkNodeReady = false + + return stdout, stderr + } + + t.Run("HandlesNewContextError", func(t *testing.T) { + setup(t) + injector := di.NewInjector() + mockShell := shell.NewMockShell() + mockShell.GetProjectRootFunc = func() (string, error) { + return "", fmt.Errorf("project root error") + } + mockShell.InitializeFunc = func() error { + return nil + } + injector.Register("shell", mockShell) + + ctx := stdcontext.WithValue(stdcontext.Background(), injectorKey, injector) + rootCmd.SetContext(ctx) - // Setup command args rootCmd.SetArgs([]string{"check", "node-health", "--nodes", "10.0.0.1"}) - // When executing the command err := Execute() - // Then an error should occur if err == nil { - t.Error("Expected error, got nil") + t.Error("Expected error when NewContext fails") } - // And error should contain init message - expectedError := "Error executing check pipeline: Nothing to check. Have you run \033[1mwindsor init\033[0m?" - if err.Error() != expectedError { - t.Errorf("Expected error about init, got: %v", err) + if !strings.Contains(err.Error(), "failed to initialize context") { + t.Errorf("Expected error about context initialization, got: %v", err) } }) - t.Run("NoNodesSpecified", func(t *testing.T) { - // Given a directory with proper configuration - _, _ = setup(t, true) + t.Run("HandlesCheckTrustedDirectoryError", func(t *testing.T) { + setup(t) + mocks := setupMocks(t) + mocks.Shell.CheckTrustedDirectoryFunc = func() error { + return fmt.Errorf("not trusted") + } - // Setup command args without nodes - rootCmd.SetArgs([]string{"check", "node-health"}) + ctx := stdcontext.WithValue(stdcontext.Background(), injectorKey, mocks.Injector) + rootCmd.SetContext(ctx) + + rootCmd.SetArgs([]string{"check", "node-health", "--nodes", "10.0.0.1"}) - // When executing the command err := Execute() - // Then an error should occur if err == nil { - t.Error("Expected error, got nil") + t.Error("Expected error when CheckTrustedDirectory fails") } - // And error should contain health checks message - expectedError := "No health checks specified. Use --nodes and/or --k8s-endpoint flags to specify health checks to perform" - if err.Error() != expectedError { - t.Errorf("Expected error about health checks, got: %v", err) + if !strings.Contains(err.Error(), "not in a trusted directory") { + t.Errorf("Expected error about trusted directory, got: %v", err) } }) - t.Run("EmptyNodesFlag", func(t *testing.T) { - // Given a directory with proper configuration - _, _ = setup(t, true) + t.Run("HandlesLoadConfigError", func(t *testing.T) { + setup(t) + injector := di.NewInjector() + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.LoadConfigFunc = func() error { + return fmt.Errorf("config load failed") + } + mockConfigHandler.InitializeFunc = func() error { + return nil + } + mockConfigHandler.GetContextFunc = func() string { + return "test-context" + } + injector.Register("configHandler", mockConfigHandler) - // Setup command args with empty nodes flag - rootCmd.SetArgs([]string{"check", "node-health", "--nodes", ""}) + mockShell := shell.NewMockShell() + mockShell.GetProjectRootFunc = func() (string, error) { + return t.TempDir(), nil + } + mockShell.CheckTrustedDirectoryFunc = func() error { + return nil + } + mockShell.InitializeFunc = func() error { + return nil + } + injector.Register("shell", mockShell) + + ctx := stdcontext.WithValue(stdcontext.Background(), injectorKey, injector) + rootCmd.SetContext(ctx) + + rootCmd.SetArgs([]string{"check", "node-health", "--nodes", "10.0.0.1"}) - // When executing the command err := Execute() - // Then an error should occur if err == nil { - t.Error("Expected error, got nil") + t.Error("Expected error when LoadConfig fails") } - // And error should contain health checks message - expectedError := "No health checks specified. Use --nodes and/or --k8s-endpoint flags to specify health checks to perform" - if err.Error() != expectedError { - t.Errorf("Expected error about health checks, got: %v", err) + if !strings.Contains(err.Error(), "config load failed") && !strings.Contains(err.Error(), "failed to load config") { + t.Errorf("Expected error about config loading, got: %v", err) + } + }) + + t.Run("HandlesCheckNodeHealthError", func(t *testing.T) { + setup(t) + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.LoadConfigFunc = func() error { + return nil + } + mockConfigHandler.InitializeFunc = func() error { + return nil + } + mockConfigHandler.GetContextFunc = func() string { + return "test-context" + } + mockConfigHandler.IsLoadedFunc = func() bool { + return true + } + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "cluster.driver" { + return "talos" + } + return "" + } + mocks := setupMocks(t, &SetupOptions{ConfigHandler: mockConfigHandler}) + + mockClusterClient := cluster.NewMockClusterClient() + mockClusterClient.WaitForNodesHealthyFunc = func(ctx stdcontext.Context, nodeAddresses []string, expectedVersion string) error { + return fmt.Errorf("cluster health check failed") + } + mocks.Injector.Register("clusterClient", mockClusterClient) + + ctx := stdcontext.WithValue(stdcontext.Background(), injectorKey, mocks.Injector) + rootCmd.SetContext(ctx) + + rootCmd.SetArgs([]string{"check", "node-health", "--nodes", "10.0.0.1"}) + + err := Execute() + + if err == nil { + t.Error("Expected error when CheckNodeHealth fails") + } + + if !strings.Contains(err.Error(), "error checking node health") && !strings.Contains(err.Error(), "nodes failed health check") { + t.Errorf("Expected error about node health check, got: %v", err) + } + }) + + t.Run("HandlesKubernetesManagerError", func(t *testing.T) { + setup(t) + nodeHealthNodes = []string{} + nodeHealthTimeout = 0 + nodeHealthVersion = "" + k8sEndpoint = "" + checkNodeReady = false + + checkNodeHealthCmd.ResetFlags() + checkNodeHealthCmd.Flags().DurationVar(&nodeHealthTimeout, "timeout", 0, "Maximum time to wait for nodes to be ready (default 5m)") + checkNodeHealthCmd.Flags().StringSliceVar(&nodeHealthNodes, "nodes", []string{}, "Nodes to check (optional)") + checkNodeHealthCmd.Flags().StringVar(&nodeHealthVersion, "version", "", "Expected version to check against (optional)") + checkNodeHealthCmd.Flags().StringVar(&k8sEndpoint, "k8s-endpoint", "", "Perform Kubernetes API health check (use --k8s-endpoint or --k8s-endpoint=https://endpoint:6443)") + checkNodeHealthCmd.Flags().Lookup("k8s-endpoint").NoOptDefVal = "true" + checkNodeHealthCmd.Flags().BoolVar(&checkNodeReady, "ready", false, "Check Kubernetes node readiness status") + + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.LoadConfigFunc = func() error { + return nil + } + mockConfigHandler.InitializeFunc = func() error { + return nil + } + mockConfigHandler.GetContextFunc = func() string { + return "test-context" + } + mockConfigHandler.IsLoadedFunc = func() bool { + return true + } + mocks := setupMocks(t, &SetupOptions{ConfigHandler: mockConfigHandler}) + + mockKubernetesManager := kubernetes.NewMockKubernetesManager(mocks.Injector) + mockKubernetesManager.InitializeFunc = func() error { + return nil + } + mockKubernetesManager.WaitForKubernetesHealthyFunc = func(ctx stdcontext.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { + return fmt.Errorf("kubernetes health check failed") + } + mocks.Injector.Register("kubernetesManager", mockKubernetesManager) + + ctx := stdcontext.WithValue(stdcontext.Background(), injectorKey, mocks.Injector) + rootCmd.SetContext(ctx) + + rootCmd.SetArgs([]string{"check", "node-health", "--k8s-endpoint", "https://test:6443"}) + + err := Execute() + + if err == nil { + t.Error("Expected error when Kubernetes health check fails") + } + + if !strings.Contains(err.Error(), "error checking node health") && !strings.Contains(err.Error(), "kubernetes health check failed") { + t.Errorf("Expected error about kubernetes health check, got: %v", err) } }) } diff --git a/pkg/context/context.go b/pkg/context/context.go index bb92a1ed0..6bc14805a 100644 --- a/pkg/context/context.go +++ b/pkg/context/context.go @@ -322,6 +322,28 @@ func (ctx *ExecutionContext) GetAliases() map[string]string { return result } +// CheckTools performs tool version checking using the tools manager. +// It validates that all required tools are installed and meet minimum version requirements. +// The tools manager must be initialized before calling this method. Returns an error if +// the tools manager is not available or if tool checking fails. +func (ctx *ExecutionContext) CheckTools() error { + if ctx.ToolsManager == nil { + ctx.initializeToolsManager() + if ctx.ToolsManager == nil { + return fmt.Errorf("tools manager not available") + } + if err := ctx.ToolsManager.Initialize(); err != nil { + return fmt.Errorf("failed to initialize tools manager: %w", err) + } + } + + if err := ctx.ToolsManager.Check(); err != nil { + return fmt.Errorf("error checking tools: %w", err) + } + + return nil +} + // GetBuildID retrieves the current build ID from the .windsor/.build-id file. // If no build ID exists, a new one is generated, persisted, and returned. // Returns the build ID string or an error if retrieval or persistence fails. @@ -422,9 +444,16 @@ func (ctx *ExecutionContext) initializeEnvPrinters() { } // initializeToolsManager initializes the tools manager if not already set. +// It checks the injector for an existing tools manager first, and only creates a new one if not found. // It creates a new ToolsManager instance and registers it with the dependency injector. func (ctx *ExecutionContext) initializeToolsManager() { if ctx.ToolsManager == nil { + if existingManager := ctx.Injector.Resolve("toolsManager"); existingManager != nil { + if toolsManager, ok := existingManager.(tools.ToolsManager); ok { + ctx.ToolsManager = toolsManager + return + } + } ctx.ToolsManager = tools.NewToolsManager(ctx.Injector) ctx.Injector.Register("toolsManager", ctx.ToolsManager) } diff --git a/pkg/context/context_test.go b/pkg/context/context_test.go index 6b8f33a0a..988af8be6 100644 --- a/pkg/context/context_test.go +++ b/pkg/context/context_test.go @@ -875,3 +875,109 @@ func (m *MockEnvPrinter) Reset() { m.ResetFunc() } } + +// ============================================================================= +// Test CheckTools +// ============================================================================= + +func TestExecutionContext_CheckTools(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + mockToolsManager := &MockToolsManager{} + mockToolsManager.InitializeFunc = func() error { + return nil + } + mockToolsManager.CheckFunc = func() error { + return nil + } + ctx.ToolsManager = mockToolsManager + + err := ctx.CheckTools() + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + }) + + t.Run("InitializesToolsManagerWhenNil", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + return false + } + mockConfigHandler.GetFunc = func(key string) interface{} { + return nil + } + + ctx.ToolsManager = nil + + err := ctx.CheckTools() + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if ctx.ToolsManager == nil { + t.Error("Expected ToolsManager to be initialized") + } + }) + + t.Run("HandlesToolsManagerInitializationError", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + ctx.ToolsManager = nil + + mockToolsManager := &MockToolsManager{} + mockToolsManager.InitializeFunc = func() error { + return errors.New("initialization failed") + } + mockToolsManager.CheckFunc = func() error { + return nil + } + mocks.Injector.Register("toolsManager", mockToolsManager) + + err := ctx.CheckTools() + + if err == nil { + t.Error("Expected error when ToolsManager initialization fails") + } + + if !strings.Contains(err.Error(), "failed to initialize tools manager") { + t.Errorf("Expected error about tools manager initialization, got: %v", err) + } + }) + + t.Run("HandlesToolsManagerCheckError", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + mockToolsManager := &MockToolsManager{} + mockToolsManager.InitializeFunc = func() error { + return nil + } + mockToolsManager.CheckFunc = func() error { + return errors.New("tools check failed") + } + ctx.ToolsManager = mockToolsManager + + err := ctx.CheckTools() + + if err == nil { + t.Error("Expected error when ToolsManager.Check fails") + } + + if !strings.Contains(err.Error(), "error checking tools") { + t.Errorf("Expected error about tools check, got: %v", err) + } + + if !strings.Contains(err.Error(), "tools check failed") { + t.Errorf("Expected error to contain original error, got: %v", err) + } + }) + +} diff --git a/pkg/provisioner/provisioner.go b/pkg/provisioner/provisioner.go index 5db2a8304..0e429e047 100644 --- a/pkg/provisioner/provisioner.go +++ b/pkg/provisioner/provisioner.go @@ -1,11 +1,13 @@ package provisioner import ( + "context" "fmt" + "time" blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" "github.com/windsorcli/cli/pkg/constants" - "github.com/windsorcli/cli/pkg/context" + execcontext "github.com/windsorcli/cli/pkg/context" "github.com/windsorcli/cli/pkg/provisioner/cluster" "github.com/windsorcli/cli/pkg/provisioner/kubernetes" k8sclient "github.com/windsorcli/cli/pkg/provisioner/kubernetes/client" @@ -25,7 +27,7 @@ import ( // ProvisionerExecutionContext holds the execution context for provisioner operations. // It embeds the base ExecutionContext and includes all provisioner-specific dependencies. type ProvisionerExecutionContext struct { - context.ExecutionContext + execcontext.ExecutionContext TerraformStack terraforminfra.Stack KubernetesManager kubernetes.KubernetesManager @@ -177,6 +179,130 @@ func (i *Provisioner) Wait(blueprint *blueprintv1alpha1.Blueprint) error { return nil } +// CheckNodeHealth performs health checks for cluster nodes and Kubernetes endpoints. +// It supports checking node health via cluster client (for Talos/Omni clusters) and/or +// Kubernetes API health checks. The method handles timeout configuration, version checking, +// and node readiness verification. Returns an error if any health check fails. +func (i *Provisioner) CheckNodeHealth(ctx context.Context, options NodeHealthCheckOptions, outputFunc func(string)) error { + hasNodeCheck := len(options.Nodes) > 0 + hasK8sCheck := options.K8SEndpointProvided + + if !hasNodeCheck && !hasK8sCheck { + return fmt.Errorf("no health checks specified. Use --nodes and/or --k8s-endpoint flags to specify health checks to perform") + } + + if hasNodeCheck && i.ClusterClient == nil && !hasK8sCheck { + return fmt.Errorf("no health checks specified. Use --nodes and/or --k8s-endpoint flags to specify health checks to perform") + } + + if hasNodeCheck && i.ClusterClient != nil { + defer i.ClusterClient.Close() + + var checkCtx context.Context + var cancel context.CancelFunc + if options.Timeout > 0 { + checkCtx, cancel = context.WithTimeout(ctx, options.Timeout) + } else { + checkCtx, cancel = context.WithCancel(ctx) + } + defer cancel() + + if err := i.ClusterClient.WaitForNodesHealthy(checkCtx, options.Nodes, options.Version); err != nil { + if hasK8sCheck { + if outputFunc != nil { + outputFunc(fmt.Sprintf("Warning: Cluster client failed (%v), continuing with Kubernetes checks\n", err)) + } + } else { + return fmt.Errorf("nodes failed health check: %w", err) + } + } else { + if outputFunc != nil { + message := fmt.Sprintf("All %d nodes are healthy", len(options.Nodes)) + if options.Version != "" { + message += fmt.Sprintf(" and running version %s", options.Version) + } + outputFunc(message) + } + } + } + + if hasK8sCheck { + if i.KubernetesManager == nil { + return fmt.Errorf("no kubernetes manager found") + } + if err := i.KubernetesManager.Initialize(); err != nil { + return fmt.Errorf("failed to initialize kubernetes manager: %w", err) + } + + k8sEndpointStr := options.K8SEndpoint + if k8sEndpointStr == "true" { + k8sEndpointStr = "" + } + + var nodeNames []string + if options.CheckNodeReady { + if hasNodeCheck { + nodeNames = options.Nodes + } else { + return fmt.Errorf("--ready flag requires --nodes to be specified") + } + } + + if len(nodeNames) > 0 && outputFunc != nil { + outputFunc(fmt.Sprintf("Waiting for %d nodes to be Ready...", len(nodeNames))) + } + + if err := i.KubernetesManager.WaitForKubernetesHealthy(ctx, k8sEndpointStr, outputFunc, nodeNames...); err != nil { + return fmt.Errorf("kubernetes health check failed: %w", err) + } + + if outputFunc != nil { + if len(nodeNames) > 0 { + readyStatus, err := i.KubernetesManager.GetNodeReadyStatus(ctx, nodeNames) + allFoundAndReady := err == nil && len(readyStatus) == len(nodeNames) + for _, ready := range readyStatus { + if !ready { + allFoundAndReady = false + break + } + } + + if allFoundAndReady { + if k8sEndpointStr != "" { + outputFunc(fmt.Sprintf("Kubernetes API endpoint %s is healthy and all nodes are Ready", k8sEndpointStr)) + } else { + outputFunc("Kubernetes API endpoint (kubeconfig default) is healthy and all nodes are Ready") + } + } else { + if k8sEndpointStr != "" { + outputFunc(fmt.Sprintf("Kubernetes API endpoint %s is healthy", k8sEndpointStr)) + } else { + outputFunc("Kubernetes API endpoint (kubeconfig default) is healthy") + } + } + } else { + if k8sEndpointStr != "" { + outputFunc(fmt.Sprintf("Kubernetes API endpoint %s is healthy", k8sEndpointStr)) + } else { + outputFunc("Kubernetes API endpoint (kubeconfig default) is healthy") + } + } + } + } + + return nil +} + +// NodeHealthCheckOptions contains options for node health checking. +type NodeHealthCheckOptions struct { + Nodes []string + Timeout time.Duration + Version string + K8SEndpoint string + K8SEndpointProvided bool + CheckNodeReady bool +} + // Close releases resources held by provisioner components. // It closes cluster client connections if present. This method should be called when the // provisioner instance is no longer needed to clean up resources. diff --git a/pkg/provisioner/provisioner_test.go b/pkg/provisioner/provisioner_test.go index 81c7a9aa3..6958971e1 100644 --- a/pkg/provisioner/provisioner_test.go +++ b/pkg/provisioner/provisioner_test.go @@ -1,9 +1,11 @@ package provisioner import ( + stdcontext "context" "fmt" "strings" "testing" + "time" blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" "github.com/windsorcli/cli/pkg/context/config" @@ -698,3 +700,395 @@ func TestProvisionerExecutionContext(t *testing.T) { } }) } + +// ============================================================================= +// Test CheckNodeHealth +// ============================================================================= + +func TestProvisioner_CheckNodeHealth(t *testing.T) { + t.Run("SuccessWithNodeCheckOnly", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + provisioner := NewProvisioner(mocks.ProvisionerExecutionContext) + + var outputMessages []string + outputFunc := func(msg string) { + outputMessages = append(outputMessages, msg) + } + + mocks.ClusterClient.WaitForNodesHealthyFunc = func(ctx stdcontext.Context, nodeAddresses []string, expectedVersion string) error { + return nil + } + + options := NodeHealthCheckOptions{ + Nodes: []string{"10.0.0.1", "10.0.0.2"}, + K8SEndpointProvided: false, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, outputFunc) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if len(outputMessages) != 1 { + t.Errorf("Expected 1 output message, got: %d", len(outputMessages)) + } + + if !strings.Contains(outputMessages[0], "All 2 nodes are healthy") { + t.Errorf("Expected output about healthy nodes, got: %q", outputMessages[0]) + } + }) + + t.Run("SuccessWithNodeCheckAndVersion", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + provisioner := NewProvisioner(mocks.ProvisionerExecutionContext) + + var outputMessages []string + outputFunc := func(msg string) { + outputMessages = append(outputMessages, msg) + } + + mocks.ClusterClient.WaitForNodesHealthyFunc = func(ctx stdcontext.Context, nodeAddresses []string, expectedVersion string) error { + if expectedVersion != "v1.5.0" { + t.Errorf("Expected version 'v1.5.0', got: %q", expectedVersion) + } + return nil + } + + options := NodeHealthCheckOptions{ + Nodes: []string{"10.0.0.1"}, + Version: "v1.5.0", + K8SEndpointProvided: false, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, outputFunc) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if len(outputMessages) != 1 { + t.Errorf("Expected 1 output message, got: %d", len(outputMessages)) + } + + if !strings.Contains(outputMessages[0], "v1.5.0") { + t.Errorf("Expected output about version, got: %q", outputMessages[0]) + } + }) + + t.Run("SuccessWithKubernetesCheckOnly", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + provisioner := NewProvisioner(mocks.ProvisionerExecutionContext) + + var outputMessages []string + outputFunc := func(msg string) { + outputMessages = append(outputMessages, msg) + } + + mocks.KubernetesManager.InitializeFunc = func() error { + return nil + } + mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx stdcontext.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { + return nil + } + + options := NodeHealthCheckOptions{ + K8SEndpoint: "https://test:6443", + K8SEndpointProvided: true, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, outputFunc) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if len(outputMessages) != 1 { + t.Errorf("Expected 1 output message, got: %d", len(outputMessages)) + } + + if !strings.Contains(outputMessages[0], "healthy") { + t.Errorf("Expected output about healthy endpoint, got: %q", outputMessages[0]) + } + }) + + t.Run("SuccessWithBothChecks", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + provisioner := NewProvisioner(mocks.ProvisionerExecutionContext) + + var outputMessages []string + outputFunc := func(msg string) { + outputMessages = append(outputMessages, msg) + } + + mocks.ClusterClient.WaitForNodesHealthyFunc = func(ctx stdcontext.Context, nodeAddresses []string, expectedVersion string) error { + return nil + } + mocks.KubernetesManager.InitializeFunc = func() error { + return nil + } + mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx stdcontext.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { + return nil + } + + options := NodeHealthCheckOptions{ + Nodes: []string{"10.0.0.1"}, + K8SEndpoint: "https://test:6443", + K8SEndpointProvided: true, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, outputFunc) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + }) + + t.Run("SuccessWithNodeReadinessCheck", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + provisioner := NewProvisioner(mocks.ProvisionerExecutionContext) + + var outputMessages []string + outputFunc := func(msg string) { + outputMessages = append(outputMessages, msg) + } + + mocks.KubernetesManager.InitializeFunc = func() error { + return nil + } + mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx stdcontext.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { + if len(nodeNames) != 1 || nodeNames[0] != "10.0.0.1" { + t.Errorf("Expected node name '10.0.0.1', got: %v", nodeNames) + } + return nil + } + mocks.KubernetesManager.GetNodeReadyStatusFunc = func(ctx stdcontext.Context, nodeNames []string) (map[string]bool, error) { + return map[string]bool{"10.0.0.1": true}, nil + } + + options := NodeHealthCheckOptions{ + Nodes: []string{"10.0.0.1"}, + K8SEndpoint: "https://test:6443", + K8SEndpointProvided: true, + CheckNodeReady: true, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, outputFunc) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + }) + + t.Run("ErrorNoHealthChecksSpecified", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + provisioner := NewProvisioner(mocks.ProvisionerExecutionContext) + + options := NodeHealthCheckOptions{ + K8SEndpointProvided: false, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, nil) + + if err == nil { + t.Error("Expected error when no health checks specified") + } + + if !strings.Contains(err.Error(), "no health checks specified") { + t.Errorf("Expected error about no health checks, got: %v", err) + } + }) + + t.Run("ErrorClusterClientWaitForNodesHealthy", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + provisioner := NewProvisioner(mocks.ProvisionerExecutionContext) + + mocks.ClusterClient.WaitForNodesHealthyFunc = func(ctx stdcontext.Context, nodeAddresses []string, expectedVersion string) error { + return fmt.Errorf("cluster health check failed") + } + + options := NodeHealthCheckOptions{ + Nodes: []string{"10.0.0.1"}, + K8SEndpointProvided: false, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, nil) + + if err == nil { + t.Error("Expected error when cluster health check fails") + } + + if !strings.Contains(err.Error(), "nodes failed health check") { + t.Errorf("Expected error about nodes failed health check, got: %v", err) + } + }) + + t.Run("WarningClusterClientFailureWithK8sCheck", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + provisioner := NewProvisioner(mocks.ProvisionerExecutionContext) + + var outputMessages []string + outputFunc := func(msg string) { + outputMessages = append(outputMessages, msg) + } + + mocks.ClusterClient.WaitForNodesHealthyFunc = func(ctx stdcontext.Context, nodeAddresses []string, expectedVersion string) error { + return fmt.Errorf("cluster health check failed") + } + mocks.KubernetesManager.InitializeFunc = func() error { + return nil + } + mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx stdcontext.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { + return nil + } + + options := NodeHealthCheckOptions{ + Nodes: []string{"10.0.0.1"}, + K8SEndpoint: "https://test:6443", + K8SEndpointProvided: true, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, outputFunc) + + if err != nil { + t.Errorf("Expected no error (cluster failure should be warning), got: %v", err) + } + + warningFound := false + for _, msg := range outputMessages { + if strings.Contains(msg, "Warning") && strings.Contains(msg, "Cluster client failed") { + warningFound = true + break + } + } + + if !warningFound { + t.Error("Expected warning message about cluster client failure") + } + }) + + t.Run("ErrorKubernetesManagerInitialize", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + provisioner := NewProvisioner(mocks.ProvisionerExecutionContext) + + mocks.KubernetesManager.InitializeFunc = func() error { + return fmt.Errorf("initialization failed") + } + + options := NodeHealthCheckOptions{ + K8SEndpoint: "https://test:6443", + K8SEndpointProvided: true, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, nil) + + if err == nil { + t.Error("Expected error when Kubernetes manager initialization fails") + } + + if !strings.Contains(err.Error(), "failed to initialize kubernetes manager") { + t.Errorf("Expected error about initialization failure, got: %v", err) + } + }) + + t.Run("ErrorKubernetesManagerWaitForKubernetesHealthy", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + provisioner := NewProvisioner(mocks.ProvisionerExecutionContext) + + mocks.KubernetesManager.InitializeFunc = func() error { + return nil + } + mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx stdcontext.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { + return fmt.Errorf("kubernetes health check failed") + } + + options := NodeHealthCheckOptions{ + K8SEndpoint: "https://test:6443", + K8SEndpointProvided: true, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, nil) + + if err == nil { + t.Error("Expected error when Kubernetes health check fails") + } + + if !strings.Contains(err.Error(), "kubernetes health check failed") { + t.Errorf("Expected error about kubernetes health check, got: %v", err) + } + }) + + t.Run("ErrorCheckNodeReadyRequiresNodes", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + provisioner := NewProvisioner(mocks.ProvisionerExecutionContext) + + mocks.KubernetesManager.InitializeFunc = func() error { + return nil + } + + options := NodeHealthCheckOptions{ + K8SEndpoint: "https://test:6443", + K8SEndpointProvided: true, + CheckNodeReady: true, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, nil) + + if err == nil { + t.Error("Expected error when --ready flag used without --nodes") + } + + if !strings.Contains(err.Error(), "--ready flag requires --nodes") { + t.Errorf("Expected error about --ready requiring --nodes, got: %v", err) + } + }) + + t.Run("ErrorNoKubernetesManager", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + provisioner := NewProvisioner(mocks.ProvisionerExecutionContext) + provisioner.KubernetesManager = nil + + options := NodeHealthCheckOptions{ + K8SEndpoint: "https://test:6443", + K8SEndpointProvided: true, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, nil) + + if err == nil { + t.Error("Expected error when Kubernetes manager is nil") + } + + if !strings.Contains(err.Error(), "no kubernetes manager found") { + t.Errorf("Expected error about no kubernetes manager, got: %v", err) + } + }) + + t.Run("SuccessWithDefaultTimeout", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + provisioner := NewProvisioner(mocks.ProvisionerExecutionContext) + + mocks.ClusterClient.WaitForNodesHealthyFunc = func(ctx stdcontext.Context, nodeAddresses []string, expectedVersion string) error { + deadline, ok := ctx.Deadline() + if !ok { + t.Error("Expected context to have deadline") + } + if deadline.IsZero() { + t.Error("Expected non-zero deadline") + } + return nil + } + + options := NodeHealthCheckOptions{ + Nodes: []string{"10.0.0.1"}, + Timeout: 5 * time.Minute, + K8SEndpointProvided: false, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, nil) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + }) +} From a3cfdc5a2caf8a3d5dd7b5e8d77985307325dcef Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:17:31 -0500 Subject: [PATCH 2/2] Rearrange package imports Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/provisioner/provisioner_test.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/provisioner/provisioner_test.go b/pkg/provisioner/provisioner_test.go index 6958971e1..1d53689c5 100644 --- a/pkg/provisioner/provisioner_test.go +++ b/pkg/provisioner/provisioner_test.go @@ -8,14 +8,14 @@ import ( "time" blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" - "github.com/windsorcli/cli/pkg/context/config" "github.com/windsorcli/cli/pkg/context" + "github.com/windsorcli/cli/pkg/context/config" + "github.com/windsorcli/cli/pkg/context/shell" "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/provisioner/cluster" "github.com/windsorcli/cli/pkg/provisioner/kubernetes" k8sclient "github.com/windsorcli/cli/pkg/provisioner/kubernetes/client" terraforminfra "github.com/windsorcli/cli/pkg/provisioner/terraform" - "github.com/windsorcli/cli/pkg/context/shell" ) // ============================================================================= @@ -53,14 +53,14 @@ func createTestBlueprint() *blueprintv1alpha1.Blueprint { } type Mocks struct { - Injector di.Injector - ConfigHandler config.ConfigHandler - Shell *shell.MockShell - TerraformStack *terraforminfra.MockStack - KubernetesManager *kubernetes.MockKubernetesManager - KubernetesClient k8sclient.KubernetesClient - ClusterClient *cluster.MockClusterClient - ProvisionerExecutionContext *ProvisionerExecutionContext + Injector di.Injector + ConfigHandler config.ConfigHandler + Shell *shell.MockShell + TerraformStack *terraforminfra.MockStack + KubernetesManager *kubernetes.MockKubernetesManager + KubernetesClient k8sclient.KubernetesClient + ClusterClient *cluster.MockClusterClient + ProvisionerExecutionContext *ProvisionerExecutionContext } // setupProvisionerMocks creates mock components for testing the Provisioner