From 53ca9ace828c7a43d44eae38c75d6e8665d30233 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Wed, 22 Oct 2025 15:06:38 -0400 Subject: [PATCH] refactor(runtime): Add k8s loader Adds kubernetes loading to the runtime. Loads a client, the manager, and any provider-specific cluster clients as-needed. Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/runtime/runtime_loaders.go | 41 ++++ pkg/runtime/runtime_loaders_test.go | 311 ++++++++++++++++++++++++++++ 2 files changed, 352 insertions(+) diff --git a/pkg/runtime/runtime_loaders.go b/pkg/runtime/runtime_loaders.go index da7cc37ea..f4ea76a00 100644 --- a/pkg/runtime/runtime_loaders.go +++ b/pkg/runtime/runtime_loaders.go @@ -5,8 +5,10 @@ import ( "path/filepath" secretsConfigType "github.com/windsorcli/cli/api/v1alpha1/secrets" + "github.com/windsorcli/cli/pkg/cluster" "github.com/windsorcli/cli/pkg/config" "github.com/windsorcli/cli/pkg/env" + "github.com/windsorcli/cli/pkg/kubernetes" "github.com/windsorcli/cli/pkg/secrets" "github.com/windsorcli/cli/pkg/shell" ) @@ -140,3 +142,42 @@ func (r *Runtime) LoadSecretsProviders() *Runtime { return r } + +// LoadKubernetes loads and initializes Kubernetes and cluster client dependencies for the Runtime. +// It creates and registers the Kubernetes client, cluster client, and Kubernetes manager in the Injector, +// then initializes the Kubernetes manager to establish connections to Kubernetes API and provider APIs. +// If any dependency is missing, it is constructed and registered. If initialization fails, it sets r.err and returns. +// Returns the Runtime instance with updated dependencies and error state. +func (r *Runtime) LoadKubernetes() *Runtime { + if r.err != nil { + return r + } + if r.ConfigHandler == nil { + r.err = fmt.Errorf("config handler not loaded - call LoadConfigHandler() first") + return r + } + if r.Injector.Resolve("kubernetesClient") == nil { + kubernetesClient := kubernetes.NewDynamicKubernetesClient() + r.Injector.Register("kubernetesClient", kubernetesClient) + } + + if r.ConfigHandler.GetString("cluster.driver") == "talos" { + if r.ClusterClient == nil { + r.ClusterClient = cluster.NewTalosClusterClient(r.Injector) + r.Injector.Register("clusterClient", r.ClusterClient) + } + } else { + r.err = fmt.Errorf("unsupported cluster driver: %s", r.ConfigHandler.GetString("cluster.driver")) + return r + } + + if r.K8sManager == nil { + r.K8sManager = kubernetes.NewKubernetesManager(r.Injector) + r.Injector.Register("kubernetesManager", r.K8sManager) + } + if err := r.K8sManager.Initialize(); err != nil { + r.err = fmt.Errorf("failed to initialize kubernetes manager: %w", err) + return r + } + return r +} diff --git a/pkg/runtime/runtime_loaders_test.go b/pkg/runtime/runtime_loaders_test.go index 4e1319dde..e035c9c8f 100644 --- a/pkg/runtime/runtime_loaders_test.go +++ b/pkg/runtime/runtime_loaders_test.go @@ -7,8 +7,10 @@ import ( "testing" secretsConfigType "github.com/windsorcli/cli/api/v1alpha1/secrets" + "github.com/windsorcli/cli/pkg/cluster" "github.com/windsorcli/cli/pkg/config" "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/kubernetes" "github.com/windsorcli/cli/pkg/shell" ) @@ -827,3 +829,312 @@ func TestRuntime_LoadSecretsProviders(t *testing.T) { } }) } + +func TestRuntime_LoadKubernetes(t *testing.T) { + t.Run("LoadsKubernetesSuccessfully", func(t *testing.T) { + // Given a runtime with loaded config handler + mocks := setupMocks(t) + runtime := NewRuntime(mocks).LoadShell().LoadConfigHandler() + + // And mock config handler returns "talos" for cluster driver + mockConfigHandler := runtime.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "cluster.driver" { + return "talos" + } + 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 cluster client should be loaded + if runtime.ClusterClient == nil { + t.Error("Expected cluster client to be loaded") + } + + // And cluster client should be registered in injector + clusterClient := runtime.Injector.Resolve("clusterClient") + if clusterClient == nil { + t.Error("Expected cluster 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 no error should be set + if runtime.err != nil { + t.Errorf("Expected no error, got %v", runtime.err) + } + }) + + t.Run("ReturnsErrorWhenConfigHandlerNotLoaded", func(t *testing.T) { + // Given a runtime without loaded config handler (no pre-loaded dependencies) + runtime := NewRuntime() + + // 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 error should be set + if runtime.err == nil { + t.Error("Expected error when config handler not loaded") + } + + expectedError := "config handler not loaded - call LoadConfigHandler() first" + if runtime.err.Error() != expectedError { + t.Errorf("Expected error %q, got %q", expectedError, runtime.err.Error()) + } + + // And no kubernetes components should be loaded + if runtime.ClusterClient != nil { + t.Error("Expected cluster client to not be loaded when error occurs") + } + if runtime.K8sManager != nil { + t.Error("Expected kubernetes manager to not be loaded when error occurs") + } + }) + + t.Run("ReturnsErrorWhenUnsupportedClusterDriver", func(t *testing.T) { + // Given a runtime with loaded config handler + mocks := setupMocks(t) + runtime := NewRuntime(mocks).LoadShell().LoadConfigHandler() + + // And mock config handler returns unsupported cluster driver + mockConfigHandler := runtime.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "cluster.driver" { + return "unsupported-driver" + } + 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 error should be set + if runtime.err == nil { + t.Error("Expected error when unsupported cluster driver") + } + + expectedError := "unsupported cluster driver: unsupported-driver" + if runtime.err.Error() != expectedError { + t.Errorf("Expected error %q, got %q", expectedError, runtime.err.Error()) + } + + // And no kubernetes components should be loaded + if runtime.ClusterClient != nil { + t.Error("Expected cluster client to not be loaded when error occurs") + } + if runtime.K8sManager != nil { + t.Error("Expected kubernetes manager to not be loaded when error occurs") + } + }) + + 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 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 components should not be loaded + if runtime.ClusterClient != nil { + t.Error("Expected cluster client to not be loaded when error exists") + } + if runtime.K8sManager != nil { + t.Error("Expected kubernetes manager to not be loaded when error exists") + } + + // And original error should be preserved + if runtime.err.Error() != "existing error" { + t.Errorf("Expected original error to be preserved, got %v", runtime.err) + } + }) + + t.Run("ReusesExistingKubernetesClient", func(t *testing.T) { + // Given a runtime with loaded config handler + mocks := setupMocks(t) + runtime := NewRuntime(mocks).LoadShell().LoadConfigHandler() + + // And mock config handler returns "talos" for cluster driver + mockConfigHandler := runtime.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "cluster.driver" { + return "talos" + } + return "mock-string" + } + + // And an existing kubernetes client registered + existingClient := kubernetes.NewMockKubernetesClient() + runtime.Injector.Register("kubernetesClient", existingClient) + + // 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 the same kubernetes client should be reused + currentClient := runtime.Injector.Resolve("kubernetesClient") + if currentClient != existingClient { + t.Error("Expected to reuse existing kubernetes client") + } + + // And no error should be set + if runtime.err != nil { + t.Errorf("Expected no error, got %v", runtime.err) + } + }) + + t.Run("ReusesExistingClusterClient", func(t *testing.T) { + // Given a runtime with loaded config handler + mocks := setupMocks(t) + runtime := NewRuntime(mocks).LoadShell().LoadConfigHandler() + + // And mock config handler returns "talos" for cluster driver + mockConfigHandler := runtime.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "cluster.driver" { + return "talos" + } + return "mock-string" + } + + // And an existing cluster client + existingClusterClient := cluster.NewMockClusterClient() + runtime.ClusterClient = existingClusterClient + + // 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 the same cluster client should be reused + if runtime.ClusterClient != existingClusterClient { + t.Error("Expected to reuse existing cluster client") + } + + // And no error should be set + if runtime.err != nil { + t.Errorf("Expected no error, got %v", runtime.err) + } + }) + + t.Run("ReusesExistingKubernetesManager", func(t *testing.T) { + // Given a runtime with loaded config handler + mocks := setupMocks(t) + runtime := NewRuntime(mocks).LoadShell().LoadConfigHandler() + + // And mock config handler returns "talos" for cluster driver + mockConfigHandler := runtime.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "cluster.driver" { + return "talos" + } + return "mock-string" + } + + // And an existing kubernetes manager + existingManager := kubernetes.NewMockKubernetesManager(nil) + runtime.K8sManager = existingManager + + // 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 the same kubernetes manager should be reused + if runtime.K8sManager != existingManager { + t.Error("Expected to reuse existing kubernetes manager") + } + + // 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) + runtime := NewRuntime(mocks).LoadShell().LoadConfigHandler() + + // And mock config handler returns "talos" for cluster driver + mockConfigHandler := runtime.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "cluster.driver" { + return "talos" + } + return "mock-string" + } + + // And a mock kubernetes manager that fails initialization + mockManager := kubernetes.NewMockKubernetesManager(nil) + mockManager.InitializeFunc = func() error { + return errors.New("initialization failed") + } + runtime.K8sManager = mockManager + + // 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 error should be set + if runtime.err == nil { + t.Error("Expected error when kubernetes manager initialization fails") + } + + expectedError := "failed to initialize kubernetes manager: initialization failed" + if runtime.err.Error() != expectedError { + t.Errorf("Expected error %q, got %q", expectedError, runtime.err.Error()) + } + }) +}