From 2cfdf6090e911a7899d6cd85543f59c5beb11e78 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:49:55 -0500 Subject: [PATCH] refactor(project): Adds the project component for top level orchestration There is a need for some reusable high-level orchestration. In particular, both configuration and initialization must take place for several commands, and requires coordination between a several high-level objects. This refactor also involved creating a few high level convenience methods on `pkg/context`. Test coverage was also expanded broadly on `pkg/context`. Improves some steps in the "up" pipeline including the blueprint installation, workstation configurations. Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- cmd/init.go | 224 +------ cmd/init_test.go | 10 +- cmd/up.go | 78 +-- cmd/up_test.go | 334 ++++------ pkg/context/context.go | 148 +++++ pkg/context/context_test.go | 1091 ++++++++++++++++++++++++++++++++ pkg/project/project.go | 165 +++++ pkg/project/project_test.go | 571 +++++++++++++++++ pkg/provisioner/provisioner.go | 34 +- pkg/workstation/workstation.go | 30 +- 10 files changed, 2217 insertions(+), 468 deletions(-) create mode 100644 pkg/project/project.go create mode 100644 pkg/project/project_test.go diff --git a/cmd/init.go b/cmd/init.go index 6d8feb83b..071ceb8f5 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -3,160 +3,14 @@ package cmd import ( "fmt" "os" - "runtime" "strings" "github.com/spf13/cobra" - "github.com/windsorcli/cli/pkg/composer" "github.com/windsorcli/cli/pkg/context" - "github.com/windsorcli/cli/pkg/context/config" "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/provisioner" - "github.com/windsorcli/cli/pkg/workstation" + "github.com/windsorcli/cli/pkg/project" ) -// ============================================================================= -// Shared Init Logic -// ============================================================================= - -// runInit performs the common initialization logic for init, up, and down commands. -// It creates execution contexts, sets up infrastructure dependencies, applies default configs, -// generates configurations, and persists the config state. -func runInit(injector di.Injector, contextName string, overwrite bool) error { - baseCtx := &context.ExecutionContext{ - Injector: injector, - } - - baseCtx, err := context.NewContext(baseCtx) - if err != nil { - return fmt.Errorf("failed to initialize context: %w", err) - } - - if err := baseCtx.Shell.AddCurrentDirToTrustedFile(); err != nil { - return fmt.Errorf("failed to add current directory to trusted file: %w", err) - } - - configHandler := baseCtx.ConfigHandler - - if err := configHandler.Initialize(); err != nil { - return fmt.Errorf("failed to initialize config handler: %w", err) - } - - if err := configHandler.SetContext(contextName); err != nil { - return fmt.Errorf("failed to set context: %w", err) - } - - if !configHandler.IsLoaded() { - existingProvider := configHandler.GetString("provider") - isDevMode := configHandler.IsDevMode(contextName) - - if isDevMode { - if err := configHandler.Set("dev", true); err != nil { - return fmt.Errorf("failed to set dev mode: %w", err) - } - } - - vmDriver := configHandler.GetString("vm.driver") - if isDevMode && vmDriver == "" { - switch runtime.GOOS { - case "darwin", "windows": - vmDriver = "docker-desktop" - default: - vmDriver = "docker" - } - } - - if vmDriver == "docker-desktop" { - if err := configHandler.SetDefault(config.DefaultConfig_Localhost); err != nil { - return fmt.Errorf("failed to set default config: %w", err) - } - } else if isDevMode { - if err := configHandler.SetDefault(config.DefaultConfig_Full); err != nil { - return fmt.Errorf("failed to set default config: %w", err) - } - } else { - if err := configHandler.SetDefault(config.DefaultConfig); err != nil { - return fmt.Errorf("failed to set default config: %w", err) - } - } - - if isDevMode && configHandler.GetString("vm.driver") == "" && vmDriver != "" { - if err := configHandler.Set("vm.driver", vmDriver); err != nil { - return fmt.Errorf("failed to set vm.driver: %w", err) - } - } - - if existingProvider == "" && isDevMode { - if err := configHandler.Set("provider", "generic"); err != nil { - return fmt.Errorf("failed to set provider from context name: %w", err) - } - } - } - - provider := configHandler.GetString("provider") - if provider != "" { - switch provider { - case "aws": - if err := configHandler.Set("aws.enabled", true); err != nil { - return fmt.Errorf("failed to set aws.enabled: %w", err) - } - if err := configHandler.Set("cluster.driver", "eks"); err != nil { - return fmt.Errorf("failed to set cluster.driver: %w", err) - } - case "azure": - if err := configHandler.Set("azure.enabled", true); err != nil { - return fmt.Errorf("failed to set azure.enabled: %w", err) - } - if err := configHandler.Set("cluster.driver", "aks"); err != nil { - return fmt.Errorf("failed to set cluster.driver: %w", err) - } - case "generic": - if err := configHandler.Set("cluster.driver", "talos"); err != nil { - return fmt.Errorf("failed to set cluster.driver: %w", err) - } - } - } - - provCtx := &provisioner.ProvisionerExecutionContext{ - ExecutionContext: *baseCtx, - } - _ = provisioner.NewProvisioner(provCtx) - - if configHandler.IsDevMode(contextName) { - workstationCtx := &workstation.WorkstationExecutionContext{ - ExecutionContext: *baseCtx, - } - _, err = workstation.NewWorkstation(workstationCtx, injector) - if err != nil { - return fmt.Errorf("failed to initialize workstation: %w", err) - } - } - - if err := configHandler.GenerateContextID(); err != nil { - return fmt.Errorf("failed to generate context ID: %w", err) - } - - if err := configHandler.SaveConfig(); err != nil { - return fmt.Errorf("failed to save config: %w", err) - } - - if err := configHandler.LoadConfig(); err != nil { - return fmt.Errorf("failed to reload context config: %w", err) - } - - composerCtx := &composer.ComposerExecutionContext{ - ExecutionContext: *baseCtx, - } - - comp := composer.NewComposer(composerCtx) - - if err := comp.Generate(overwrite); err != nil { - return fmt.Errorf("failed to generate infrastructure: %w", err) - } - - return nil -} - // ============================================================================= // Init Command // ============================================================================= @@ -198,6 +52,10 @@ var initCmd = &cobra.Command{ return fmt.Errorf("failed to initialize context: %w", err) } + if err := baseCtx.Shell.AddCurrentDirToTrustedFile(); err != nil { + return fmt.Errorf("failed to add current directory to trusted file: %w", err) + } + contextName := "local" if len(args) > 0 { contextName = args[0] @@ -213,83 +71,67 @@ var initCmd = &cobra.Command{ initProvider = initPlatform } - if baseCtx.ConfigHandler.IsDevMode(contextName) && initProvider == "" { - initProvider = "generic" - } - - configHandler := baseCtx.ConfigHandler - + // Build flag overrides map + flagOverrides := make(map[string]any) if initProvider != "" { - if err := configHandler.Set("provider", initProvider); err != nil { - return fmt.Errorf("failed to set provider: %w", err) - } + flagOverrides["provider"] = initProvider } if initBackend != "" { - if err := configHandler.Set("terraform.backend.type", initBackend); err != nil { - return fmt.Errorf("failed to set terraform.backend.type: %w", err) - } + flagOverrides["terraform.backend.type"] = initBackend } if initAwsProfile != "" { - if err := configHandler.Set("aws.profile", initAwsProfile); err != nil { - return fmt.Errorf("failed to set aws.profile: %w", err) - } + flagOverrides["aws.profile"] = initAwsProfile } if initAwsEndpointURL != "" { - if err := configHandler.Set("aws.endpoint_url", initAwsEndpointURL); err != nil { - return fmt.Errorf("failed to set aws.endpoint_url: %w", err) - } + flagOverrides["aws.endpoint_url"] = initAwsEndpointURL } if initVmDriver != "" { - if err := configHandler.Set("vm.driver", initVmDriver); err != nil { - return fmt.Errorf("failed to set vm.driver: %w", err) - } + flagOverrides["vm.driver"] = initVmDriver } if initCpu > 0 { - if err := configHandler.Set("vm.cpu", initCpu); err != nil { - return fmt.Errorf("failed to set vm.cpu: %w", err) - } + flagOverrides["vm.cpu"] = initCpu } if initDisk > 0 { - if err := configHandler.Set("vm.disk", initDisk); err != nil { - return fmt.Errorf("failed to set vm.disk: %w", err) - } + flagOverrides["vm.disk"] = initDisk } if initMemory > 0 { - if err := configHandler.Set("vm.memory", initMemory); err != nil { - return fmt.Errorf("failed to set vm.memory: %w", err) - } + flagOverrides["vm.memory"] = initMemory } if initArch != "" { - if err := configHandler.Set("vm.arch", initArch); err != nil { - return fmt.Errorf("failed to set vm.arch: %w", err) - } + flagOverrides["vm.arch"] = initArch } if initDocker { - if err := configHandler.Set("docker.enabled", true); err != nil { - return fmt.Errorf("failed to set docker.enabled: %w", err) - } + flagOverrides["docker.enabled"] = true } if initGitLivereload { - if err := configHandler.Set("git.livereload.enabled", true); err != nil { - return fmt.Errorf("failed to set git.livereload.enabled: %w", err) - } + flagOverrides["git.livereload.enabled"] = true } for _, setFlag := range initSetFlags { parts := strings.SplitN(setFlag, "=", 2) if len(parts) == 2 { - if err := configHandler.Set(parts[0], parts[1]); err != nil { - return fmt.Errorf("failed to set %s: %w", parts[0], err) - } + flagOverrides[parts[0]] = parts[1] } } - if err := runInit(injector, contextName, initReset); err != nil { + proj, err := project.NewProject(injector, contextName, baseCtx) + if err != nil { + return err + } + + if err := proj.Configure(flagOverrides); err != nil { + return err + } + + if err := proj.Initialize(initReset); err != nil { + if !verbose { + return nil + } return err } hasSetFlags := len(initSetFlags) > 0 - if err := configHandler.SaveConfig(hasSetFlags); err != nil { + if err := proj.Context.ConfigHandler.SaveConfig(hasSetFlags); err != nil { return fmt.Errorf("failed to save configuration: %w", err) } diff --git a/cmd/init_test.go b/cmd/init_test.go index 5926b854a..c268e2ab3 100644 --- a/cmd/init_test.go +++ b/cmd/init_test.go @@ -10,8 +10,9 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/windsorcli/cli/pkg/composer/blueprint" - "github.com/windsorcli/cli/pkg/context/config" "github.com/windsorcli/cli/pkg/constants" + "github.com/windsorcli/cli/pkg/context/config" + "github.com/windsorcli/cli/pkg/context/tools" "github.com/windsorcli/cli/pkg/di" ) @@ -68,6 +69,13 @@ func setupInitTest(t *testing.T, opts ...*SetupOptions) *InitMocks { mockBlueprintHandler.WriteFunc = func(overwrite ...bool) error { return nil } baseMocks.Injector.Register("blueprintHandler", mockBlueprintHandler) + // Add mock tools manager (required by runInit) + mockToolsManager := tools.NewMockToolsManager() + mockToolsManager.InitializeFunc = func() error { return nil } + mockToolsManager.CheckFunc = func() error { return nil } + mockToolsManager.InstallFunc = func() error { return nil } + baseMocks.Injector.Register("toolsManager", mockToolsManager) + return &InitMocks{ Injector: baseMocks.Injector, ConfigHandler: baseMocks.ConfigHandler, diff --git a/cmd/up.go b/cmd/up.go index b4da825f5..f34605090 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -1,13 +1,11 @@ package cmd import ( - "context" "fmt" "github.com/spf13/cobra" "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/pipelines" - "github.com/windsorcli/cli/pkg/runtime" + "github.com/windsorcli/cli/pkg/project" ) var ( @@ -21,72 +19,48 @@ var upCmd = &cobra.Command{ Long: "Set up the Windsor environment by executing necessary shell commands.", SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - // Get shared dependency injector from context injector := cmd.Context().Value(injectorKey).(di.Injector) - // First, set up environment variables using runtime - deps := &runtime.Dependencies{ - Injector: injector, - } - if err := runtime.NewRuntime(deps). - LoadShell(). - CheckTrustedDirectory(). - LoadConfig(). - LoadSecretsProviders(). - LoadEnvVars(runtime.EnvVarsOptions{ - Decrypt: true, - Verbose: verbose, - }). - ExecutePostEnvHook(verbose). - Do(); err != nil { - return fmt.Errorf("failed to set up environment: %w", err) - } - - // Then, run the init pipeline to initialize the environment - var initPipeline pipelines.Pipeline - initPipeline, err := pipelines.WithPipeline(injector, cmd.Context(), "initPipeline") + proj, err := project.NewProject(injector, "") if err != nil { - return fmt.Errorf("failed to set up init pipeline: %w", err) + return err } - if err := initPipeline.Execute(cmd.Context()); err != nil { - return fmt.Errorf("failed to initialize environment: %w", err) + + if err := proj.Context.Shell.CheckTrustedDirectory(); err != nil { + return fmt.Errorf("not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve") } - // Finally, run the up pipeline for infrastructure setup - upPipeline, err := pipelines.WithPipeline(injector, cmd.Context(), "upPipeline") - if err != nil { - return fmt.Errorf("failed to set up up pipeline: %w", err) + if err := proj.Configure(nil); err != nil { + return err } - // Create execution context with flags - ctx := cmd.Context() - if installFlag { - ctx = context.WithValue(ctx, "install", true) + if err := proj.Initialize(false); err != nil { + if !verbose { + return nil + } + return err } - if waitFlag { - ctx = context.WithValue(ctx, "wait", true) + + if proj.Workstation != nil { + if err := proj.Workstation.Up(); err != nil { + return fmt.Errorf("error starting workstation: %w", err) + } } - // Execute the up pipeline - if err := upPipeline.Execute(ctx); err != nil { - return fmt.Errorf("Error executing up pipeline: %w", err) + blueprint := proj.Composer.BlueprintHandler.Generate() + if err := proj.Provisioner.Up(blueprint); err != nil { + return fmt.Errorf("error starting infrastructure: %w", err) } - // Run install pipeline if requested if installFlag { - installPipeline, err := pipelines.WithPipeline(injector, cmd.Context(), "installPipeline") - if err != nil { - return fmt.Errorf("failed to set up install pipeline: %w", err) + if err := proj.Provisioner.Install(blueprint); err != nil { + return fmt.Errorf("error installing blueprint: %w", err) } - // Create installation context with wait flag if needed - installCtx := cmd.Context() if waitFlag { - installCtx = context.WithValue(installCtx, "wait", true) - } - - if err := installPipeline.Execute(installCtx); err != nil { - return fmt.Errorf("Error executing install pipeline: %w", err) + if err := proj.Provisioner.Wait(blueprint); err != nil { + return fmt.Errorf("error waiting for kustomizations: %w", err) + } } } diff --git a/cmd/up_test.go b/cmd/up_test.go index 20545d111..d9972caca 100644 --- a/cmd/up_test.go +++ b/cmd/up_test.go @@ -9,10 +9,15 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" + blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" + "github.com/windsorcli/cli/pkg/composer/blueprint" "github.com/windsorcli/cli/pkg/context/config" - "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/pipelines" + envvars "github.com/windsorcli/cli/pkg/context/env" "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/kubernetes" + terraforminfra "github.com/windsorcli/cli/pkg/provisioner/terraform" ) // ============================================================================= @@ -20,10 +25,13 @@ import ( // ============================================================================= type UpMocks struct { - Injector di.Injector - ConfigHandler config.ConfigHandler - Shell *shell.MockShell - Shims *Shims + Injector di.Injector + ConfigHandler config.ConfigHandler + Shell *shell.MockShell + Shims *Shims + BlueprintHandler *blueprint.MockBlueprintHandler + TerraformStack *terraforminfra.MockStack + KubernetesManager *kubernetes.MockKubernetesManager } func setupUpTest(t *testing.T, opts ...*SetupOptions) *UpMocks { @@ -35,34 +43,85 @@ func setupUpTest(t *testing.T, opts ...*SetupOptions) *UpMocks { os.Chdir(tmpDir) t.Cleanup(func() { os.Chdir(oldDir) }) - // Get base mocks - baseMocks := setupMocks(t, opts...) - - // Note: envPipeline no longer used - up now uses runtime.LoadEnvVars - - // Register mock init pipeline in injector (needed since up runs init pipeline second) - mockInitPipeline := pipelines.NewMockBasePipeline() - mockInitPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockInitPipeline.ExecuteFunc = func(ctx context.Context) error { return nil } - baseMocks.Injector.Register("initPipeline", mockInitPipeline) - - // Register mock up pipeline in injector - mockUpPipeline := pipelines.NewMockBasePipeline() - mockUpPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockUpPipeline.ExecuteFunc = func(ctx context.Context) error { return nil } - baseMocks.Injector.Register("upPipeline", mockUpPipeline) - - // Register mock install pipeline in injector (needed since up conditionally runs install pipeline) - mockInstallPipeline := pipelines.NewMockBasePipeline() - mockInstallPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockInstallPipeline.ExecuteFunc = func(ctx context.Context) error { return nil } - baseMocks.Injector.Register("installPipeline", mockInstallPipeline) + // Create mock config handler to control IsDevMode + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.InitializeFunc = func() error { return nil } + mockConfigHandler.GetContextFunc = func() string { return "test-context" } + mockConfigHandler.IsDevModeFunc = func(contextName string) bool { return false } + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if len(defaultValue) > 0 { + return defaultValue[0] + } + return "" + } + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + if len(defaultValue) > 0 { + return defaultValue[0] + } + return false + } + mockConfigHandler.IsLoadedFunc = func() bool { return true } + mockConfigHandler.LoadConfigFunc = func() error { return nil } + mockConfigHandler.SaveConfigFunc = func(hasSetFlags ...bool) error { return nil } + mockConfigHandler.GenerateContextIDFunc = func() error { return nil } + mockConfigHandler.GetConfigRootFunc = func() (string, error) { return tmpDir + "/contexts/test-context", nil } + + // Get base mocks with mock config handler + testOpts := &SetupOptions{} + if len(opts) > 0 && opts[0] != nil { + testOpts = opts[0] + } + testOpts.ConfigHandler = mockConfigHandler + baseMocks := setupMocks(t, testOpts) + + // Add up-specific shell mock behaviors + baseMocks.Shell.CheckTrustedDirectoryFunc = func() error { return nil } + + // Add blueprint handler mock + mockBlueprintHandler := blueprint.NewMockBlueprintHandler(baseMocks.Injector) + mockBlueprintHandler.InitializeFunc = func() error { return nil } + mockBlueprintHandler.LoadBlueprintFunc = func() error { return nil } + mockBlueprintHandler.WriteFunc = func(overwrite ...bool) error { return nil } + mockBlueprintHandler.LoadConfigFunc = func() error { return nil } + testBlueprint := &blueprintv1alpha1.Blueprint{ + Metadata: blueprintv1alpha1.Metadata{Name: "test"}, + } + mockBlueprintHandler.GenerateFunc = func() *blueprintv1alpha1.Blueprint { return testBlueprint } + baseMocks.Injector.Register("blueprintHandler", mockBlueprintHandler) + + // Add terraform stack mock + mockTerraformStack := terraforminfra.NewMockStack(baseMocks.Injector) + mockTerraformStack.InitializeFunc = func() error { return nil } + mockTerraformStack.UpFunc = func(blueprint *blueprintv1alpha1.Blueprint) error { return nil } + mockTerraformStack.DownFunc = func(blueprint *blueprintv1alpha1.Blueprint) error { return nil } + baseMocks.Injector.Register("terraformStack", mockTerraformStack) + + // Add kubernetes manager mock + mockKubernetesManager := kubernetes.NewMockKubernetesManager(baseMocks.Injector) + mockKubernetesManager.InitializeFunc = func() error { return nil } + mockKubernetesManager.ApplyBlueprintFunc = func(blueprint *blueprintv1alpha1.Blueprint, namespace string) error { return nil } + mockKubernetesManager.WaitForKustomizationsFunc = func(message string, names ...string) error { return nil } + baseMocks.Injector.Register("kubernetesManager", mockKubernetesManager) + + // Add terraform env printer (required by terraform stack) + terraformEnvPrinter := envvars.NewTerraformEnvPrinter(baseMocks.Injector) + baseMocks.Injector.Register("terraformEnv", terraformEnvPrinter) + + // Add mock tools manager (required by runInit) + mockToolsManager := tools.NewMockToolsManager() + mockToolsManager.InitializeFunc = func() error { return nil } + mockToolsManager.CheckFunc = func() error { return nil } + mockToolsManager.InstallFunc = func() error { return nil } + baseMocks.Injector.Register("toolsManager", mockToolsManager) return &UpMocks{ - Injector: baseMocks.Injector, - ConfigHandler: baseMocks.ConfigHandler, - Shell: baseMocks.Shell, - Shims: baseMocks.Shims, + Injector: baseMocks.Injector, + ConfigHandler: baseMocks.ConfigHandler, + Shell: baseMocks.Shell, + Shims: baseMocks.Shims, + BlueprintHandler: mockBlueprintHandler, + TerraformStack: mockTerraformStack, + KubernetesManager: mockKubernetesManager, } } @@ -136,7 +195,7 @@ func TestUpCmd(t *testing.T) { cmd.SetContext(ctx) err := cmd.Execute() - // Then no error should occur + // Then no error should occur (wait only works with install) if err != nil { t.Errorf("Expected success, got error: %v", err) } @@ -159,37 +218,14 @@ func TestUpCmd(t *testing.T) { } }) - t.Run("SuccessWithVerboseContext", func(t *testing.T) { - // Given a temporary directory with mocked dependencies - mocks := setupUpTest(t) - - // When executing the up command with verbose context - cmd := createTestUpCmd() - ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) - ctx = context.WithValue(ctx, "verbose", true) - cmd.SetArgs([]string{}) - cmd.SetContext(ctx) - err := cmd.Execute() - - // Then no error should occur - if err != nil { - t.Errorf("Expected success, got error: %v", err) - } - }) - - // Note: EnvPipelineExecutionError test removed - env pipeline no longer used - - t.Run("InitPipelineExecutionError", func(t *testing.T) { + t.Run("CheckTrustedDirectoryError", func(t *testing.T) { // Given a temporary directory with mocked dependencies mocks := setupUpTest(t) - // And an init pipeline that fails to execute - mockInitPipeline := pipelines.NewMockBasePipeline() - mockInitPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockInitPipeline.ExecuteFunc = func(ctx context.Context) error { - return fmt.Errorf("init pipeline execution failed") + // And CheckTrustedDirectory that fails + mocks.Shell.CheckTrustedDirectoryFunc = func() error { + return fmt.Errorf("not in trusted directory") } - mocks.Injector.Register("initPipeline", mockInitPipeline) // When executing the up command cmd := createTestUpCmd() @@ -202,22 +238,19 @@ func TestUpCmd(t *testing.T) { if err == nil { t.Error("Expected error, got nil") } - if !strings.Contains(err.Error(), "failed to initialize environment") { - t.Errorf("Expected init pipeline execution error, got: %v", err) + if !strings.Contains(err.Error(), "not in a trusted directory") { + t.Errorf("Expected trusted directory error, got: %v", err) } }) - t.Run("UpPipelineExecutionError", func(t *testing.T) { + t.Run("ProvisionerUpError", func(t *testing.T) { // Given a temporary directory with mocked dependencies mocks := setupUpTest(t) - // And an up pipeline that fails to execute - mockUpPipeline := pipelines.NewMockBasePipeline() - mockUpPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockUpPipeline.ExecuteFunc = func(ctx context.Context) error { - return fmt.Errorf("up pipeline execution failed") + // And terraform stack Up that fails + mocks.TerraformStack.UpFunc = func(blueprint *blueprintv1alpha1.Blueprint) error { + return fmt.Errorf("terraform stack up failed") } - mocks.Injector.Register("upPipeline", mockUpPipeline) // When executing the up command cmd := createTestUpCmd() @@ -229,60 +262,21 @@ func TestUpCmd(t *testing.T) { // Then an error should occur if err == nil { t.Error("Expected error, got nil") + return } - if !strings.Contains(err.Error(), "Error executing up pipeline") { - t.Errorf("Expected up pipeline execution error, got: %v", err) - } - }) - - t.Run("ContextPropagation", func(t *testing.T) { - // Given a temporary directory with mocked dependencies - mocks := setupUpTest(t) - - // And an install pipeline that validates context values - installPipelineCalled := false - waitContextPassed := false - mockInstallPipeline := pipelines.NewMockBasePipeline() - mockInstallPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockInstallPipeline.ExecuteFunc = func(ctx context.Context) error { - installPipelineCalled = true - if ctx.Value("wait") == true { - waitContextPassed = true - } - return nil - } - mocks.Injector.Register("installPipeline", mockInstallPipeline) - - // When executing the up command with install and wait flags - cmd := createTestUpCmd() - ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) - cmd.SetArgs([]string{"--install", "--wait"}) - cmd.SetContext(ctx) - err := cmd.Execute() - - // Then no error should occur and install pipeline should be called with wait context - if err != nil { - t.Errorf("Expected success, got error: %v", err) - } - if !installPipelineCalled { - t.Error("Expected install pipeline to be called when --install flag is set") - } - if !waitContextPassed { - t.Error("Expected wait context to be passed to install pipeline") + if !strings.Contains(err.Error(), "error starting infrastructure") { + t.Errorf("Expected infrastructure error, got: %v", err) } }) - t.Run("InstallPipelineExecutionError", func(t *testing.T) { + t.Run("ProvisionerInstallError", func(t *testing.T) { // Given a temporary directory with mocked dependencies mocks := setupUpTest(t) - // And an install pipeline that fails to execute - mockInstallPipeline := pipelines.NewMockBasePipeline() - mockInstallPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockInstallPipeline.ExecuteFunc = func(ctx context.Context) error { - return fmt.Errorf("install pipeline execution failed") + // And kubernetes manager ApplyBlueprint that fails + mocks.KubernetesManager.ApplyBlueprintFunc = func(blueprint *blueprintv1alpha1.Blueprint, namespace string) error { + return fmt.Errorf("kubernetes apply failed") } - mocks.Injector.Register("installPipeline", mockInstallPipeline) // When executing the up command with install flag cmd := createTestUpCmd() @@ -295,119 +289,33 @@ func TestUpCmd(t *testing.T) { if err == nil { t.Error("Expected error, got nil") } - if !strings.Contains(err.Error(), "Error executing install pipeline") { - t.Errorf("Expected install pipeline execution error, got: %v", err) + if !strings.Contains(err.Error(), "error installing blueprint") { + t.Errorf("Expected install error, got: %v", err) } }) - t.Run("VerboseContextPropagation", func(t *testing.T) { + t.Run("ProvisionerWaitError", func(t *testing.T) { // Given a temporary directory with mocked dependencies mocks := setupUpTest(t) - // And an up pipeline that validates verbose context - verboseValidated := false - mockUpPipeline := pipelines.NewMockBasePipeline() - mockUpPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockUpPipeline.ExecuteFunc = func(ctx context.Context) error { - // Verify that verbose flag is properly propagated - if ctx.Value("verbose") == true { - verboseValidated = true - } - return nil + // And kubernetes manager WaitForKustomizations that fails + mocks.KubernetesManager.WaitForKustomizationsFunc = func(message string, names ...string) error { + return fmt.Errorf("wait for kustomizations failed") } - mocks.Injector.Register("upPipeline", mockUpPipeline) - // When executing the up command with verbose flag set in context - cmd := createTestUpCmd() - ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) - ctx = context.WithValue(ctx, "verbose", true) - cmd.SetArgs([]string{}) - cmd.SetContext(ctx) - err := cmd.Execute() - - // Then no error should occur and verbose context should be validated - if err != nil { - t.Errorf("Expected success, got error: %v", err) - } - if !verboseValidated { - t.Error("Expected verbose context value to be properly propagated to up pipeline") - } - }) - - // Note: EnvPipelineContextValues test removed - env pipeline no longer used - - t.Run("MultipleFlagsCombination", func(t *testing.T) { - // Given a temporary directory with mocked dependencies - mocks := setupUpTest(t) - - // When executing the up command with multiple flags + // When executing the up command with install and wait flags cmd := createTestUpCmd() ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) - ctx = context.WithValue(ctx, "verbose", true) cmd.SetArgs([]string{"--install", "--wait"}) cmd.SetContext(ctx) err := cmd.Execute() - // Then no error should occur - if err != nil { - t.Errorf("Expected success, got error: %v", err) - } - }) - - t.Run("PipelineOrchestrationOrder", func(t *testing.T) { - // Given a temporary directory with mocked dependencies - mocks := setupUpTest(t) - - // And pipelines that track execution order - executionOrder := []string{} - - // Note: env pipeline no longer used - environment setup is handled by runtime - - mockInitPipeline := pipelines.NewMockBasePipeline() - mockInitPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockInitPipeline.ExecuteFunc = func(ctx context.Context) error { - executionOrder = append(executionOrder, "init") - return nil - } - mocks.Injector.Register("initPipeline", mockInitPipeline) - - mockUpPipeline := pipelines.NewMockBasePipeline() - mockUpPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockUpPipeline.ExecuteFunc = func(ctx context.Context) error { - executionOrder = append(executionOrder, "up") - return nil - } - mocks.Injector.Register("upPipeline", mockUpPipeline) - - mockInstallPipeline := pipelines.NewMockBasePipeline() - mockInstallPipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { return nil } - mockInstallPipeline.ExecuteFunc = func(ctx context.Context) error { - executionOrder = append(executionOrder, "install") - return nil - } - mocks.Injector.Register("installPipeline", mockInstallPipeline) - - // When executing the up command with install flag - cmd := createTestUpCmd() - ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) - cmd.SetArgs([]string{"--install"}) - cmd.SetContext(ctx) - err := cmd.Execute() - - // Then no error should occur and pipelines should execute in correct order - if err != nil { - t.Errorf("Expected success, got error: %v", err) - } - expectedOrder := []string{"init", "up", "install"} - if len(executionOrder) != len(expectedOrder) { - t.Errorf("Expected %d pipeline executions, got %d", len(expectedOrder), len(executionOrder)) + // Then an error should occur + if err == nil { + t.Error("Expected error, got nil") } - for i, expected := range expectedOrder { - if i >= len(executionOrder) || executionOrder[i] != expected { - t.Errorf("Expected pipeline execution order %v, got %v", expectedOrder, executionOrder) - break - } + if !strings.Contains(err.Error(), "error waiting for kustomizations") { + t.Errorf("Expected wait error, got: %v", err) } }) - } diff --git a/pkg/context/context.go b/pkg/context/context.go index b4b072df5..c702fd4e8 100644 --- a/pkg/context/context.go +++ b/pkg/context/context.go @@ -7,6 +7,7 @@ import ( "math/big" "os" "path/filepath" + "runtime" "strconv" "strings" "time" @@ -641,3 +642,150 @@ func (ctx *ExecutionContext) incrementBuildID(existingBuildID, currentDate strin existingCounter++ return fmt.Sprintf("%s.%s.%d", existingDate, existingRandom, existingCounter), nil } + +// ApplyConfigDefaults sets default configuration values based on context name, dev mode, and VM driver. +// It determines the appropriate default configuration (localhost, full, or standard) based on the VM driver +// and dev mode settings. For dev mode, it also sets the provider to "generic" if not already set. +// This method should be called before loading configuration from disk to ensure defaults are applied first. +// The context name is read from ctx.ContextName. Returns an error if any configuration operation fails. +func (ctx *ExecutionContext) ApplyConfigDefaults() error { + contextName := ctx.ContextName + if contextName == "" { + contextName = "local" + } + + if ctx.ConfigHandler == nil { + return fmt.Errorf("config handler not available") + } + + if !ctx.ConfigHandler.IsLoaded() { + existingProvider := ctx.ConfigHandler.GetString("provider") + contextName := ctx.ContextName + if contextName == "" { + contextName = "local" + } + isDevMode := ctx.ConfigHandler.IsDevMode(contextName) + + if isDevMode { + if err := ctx.ConfigHandler.Set("dev", true); err != nil { + return fmt.Errorf("failed to set dev mode: %w", err) + } + } + + vmDriver := ctx.ConfigHandler.GetString("vm.driver") + if isDevMode && vmDriver == "" { + switch runtime.GOOS { + case "darwin", "windows": + vmDriver = "docker-desktop" + default: + vmDriver = "docker" + } + } + + if vmDriver == "docker-desktop" { + if err := ctx.ConfigHandler.SetDefault(config.DefaultConfig_Localhost); err != nil { + return fmt.Errorf("failed to set default config: %w", err) + } + } else if isDevMode { + if err := ctx.ConfigHandler.SetDefault(config.DefaultConfig_Full); err != nil { + return fmt.Errorf("failed to set default config: %w", err) + } + } else { + if err := ctx.ConfigHandler.SetDefault(config.DefaultConfig); err != nil { + return fmt.Errorf("failed to set default config: %w", err) + } + } + + if isDevMode && ctx.ConfigHandler.GetString("vm.driver") == "" && vmDriver != "" { + if err := ctx.ConfigHandler.Set("vm.driver", vmDriver); err != nil { + return fmt.Errorf("failed to set vm.driver: %w", err) + } + } + + if existingProvider == "" && isDevMode { + if err := ctx.ConfigHandler.Set("provider", "generic"); err != nil { + return fmt.Errorf("failed to set provider from context name: %w", err) + } + } + } + + return nil +} + +// ApplyProviderDefaults sets provider-specific configuration values based on the provider type. +// For "aws", it enables AWS and sets the cluster driver to "eks". +// For "azure", it enables Azure and sets the cluster driver to "aks". +// For "generic", it sets the cluster driver to "talos". +// If no provider is set but dev mode is enabled, it defaults the cluster driver to "talos". +// The context name is read from ctx.ContextName. Returns an error if any configuration operation fails. +func (ctx *ExecutionContext) ApplyProviderDefaults(providerOverride string) error { + if ctx.ConfigHandler == nil { + return fmt.Errorf("config handler not available") + } + + contextName := ctx.ContextName + if contextName == "" { + contextName = "local" + } + + provider := providerOverride + if provider == "" { + provider = ctx.ConfigHandler.GetString("provider") + } + + if provider != "" { + switch provider { + case "aws": + if err := ctx.ConfigHandler.Set("aws.enabled", true); err != nil { + return fmt.Errorf("failed to set aws.enabled: %w", err) + } + if err := ctx.ConfigHandler.Set("cluster.driver", "eks"); err != nil { + return fmt.Errorf("failed to set cluster.driver: %w", err) + } + case "azure": + if err := ctx.ConfigHandler.Set("azure.enabled", true); err != nil { + return fmt.Errorf("failed to set azure.enabled: %w", err) + } + if err := ctx.ConfigHandler.Set("cluster.driver", "aks"); err != nil { + return fmt.Errorf("failed to set cluster.driver: %w", err) + } + case "generic": + if err := ctx.ConfigHandler.Set("cluster.driver", "talos"); err != nil { + return fmt.Errorf("failed to set cluster.driver: %w", err) + } + } + } else if ctx.ConfigHandler.IsDevMode(contextName) { + if ctx.ConfigHandler.GetString("cluster.driver") == "" { + if err := ctx.ConfigHandler.Set("cluster.driver", "talos"); err != nil { + return fmt.Errorf("failed to set cluster.driver: %w", err) + } + } + } + + return nil +} + +// PrepareTools checks and installs required tools using the tools manager. +// It first checks that all required tools are installed and meet version requirements, +// then installs any missing or outdated tools. The tools manager must be available. +// Returns an error if the tools manager is not available or if checking or installation fails. +func (ctx *ExecutionContext) PrepareTools() 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) + } + if err := ctx.ToolsManager.Install(); err != nil { + return fmt.Errorf("error installing tools: %w", err) + } + + return nil +} diff --git a/pkg/context/context_test.go b/pkg/context/context_test.go index 988af8be6..e37908111 100644 --- a/pkg/context/context_test.go +++ b/pkg/context/context_test.go @@ -2,10 +2,13 @@ package context import ( "errors" + "fmt" + "os" "path/filepath" "strings" "testing" + v1alpha1 "github.com/windsorcli/cli/api/v1alpha1" "github.com/windsorcli/cli/pkg/context/config" "github.com/windsorcli/cli/pkg/context/secrets" "github.com/windsorcli/cli/pkg/context/shell" @@ -167,6 +170,82 @@ func TestNewContext(t *testing.T) { t.Errorf("Expected TemplateRoot to be %q, got: %s", expectedTemplateRoot, ctx.TemplateRoot) } }) + + t.Run("ErrorWhenContextIsNil", func(t *testing.T) { + _, err := NewContext(nil) + + if err == nil { + t.Error("Expected error when context is nil") + } + + if !strings.Contains(err.Error(), "execution context is required") { + t.Errorf("Expected error about execution context required, got: %v", err) + } + }) + + t.Run("ErrorWhenInjectorIsNil", func(t *testing.T) { + ctx := &ExecutionContext{} + + _, err := NewContext(ctx) + + if err == nil { + t.Error("Expected error when injector is nil") + } + + if !strings.Contains(err.Error(), "injector is required") { + t.Errorf("Expected error about injector required, got: %v", err) + } + }) + + t.Run("CreatesShellWhenNotProvided", func(t *testing.T) { + injector := di.NewInjector() + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.GetContextFunc = func() string { + return "test" + } + injector.Register("configHandler", mockConfigHandler) + + ctx := &ExecutionContext{ + Injector: injector, + } + + result, err := NewContext(ctx) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if result.Shell == nil { + t.Error("Expected shell to be created") + } + }) + + t.Run("CreatesConfigHandlerWhenNotProvided", func(t *testing.T) { + injector := di.NewInjector() + mockShell := shell.NewMockShell() + mockShell.InitializeFunc = func() error { + return nil + } + mockShell.GetProjectRootFunc = func() (string, error) { + return "/test", nil + } + injector.Register("shell", mockShell) + + ctx := &ExecutionContext{ + Injector: injector, + Shell: mockShell, + } + + result, err := NewContext(ctx) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if result.ConfigHandler == nil { + t.Error("Expected config handler to be created") + } + }) } // ============================================================================= @@ -981,3 +1060,1015 @@ func TestExecutionContext_CheckTools(t *testing.T) { }) } + +func TestExecutionContext_CheckTrustedDirectory(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + mockShell := mocks.Shell.(*shell.MockShell) + mockShell.CheckTrustedDirectoryFunc = func() error { + return nil + } + + err := ctx.CheckTrustedDirectory() + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + }) + + t.Run("ErrorWhenShellNotInitialized", func(t *testing.T) { + ctx := &ExecutionContext{} + + err := ctx.CheckTrustedDirectory() + + if err == nil { + t.Error("Expected error when Shell is nil") + } + + if !strings.Contains(err.Error(), "shell not initialized") { + t.Errorf("Expected error about shell not initialized, got: %v", err) + } + }) + + t.Run("ErrorWhenCheckFails", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + mockShell := mocks.Shell.(*shell.MockShell) + mockShell.CheckTrustedDirectoryFunc = func() error { + return fmt.Errorf("directory not trusted") + } + + err := ctx.CheckTrustedDirectory() + + if err == nil { + t.Error("Expected error when check fails") + } + + if !strings.Contains(err.Error(), "directory not trusted") { + t.Errorf("Expected error about directory not trusted, got: %v", err) + } + }) +} + +func TestExecutionContext_LoadConfig(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.LoadConfigFunc = func() error { + return nil + } + + err := ctx.LoadConfig() + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + }) + + t.Run("ErrorWhenConfigHandlerNotInitialized", func(t *testing.T) { + ctx := &ExecutionContext{} + + err := ctx.LoadConfig() + + if err == nil { + t.Error("Expected error when ConfigHandler is nil") + } + + if !strings.Contains(err.Error(), "config handler not initialized") { + t.Errorf("Expected error about config handler not initialized, got: %v", err) + } + }) + + t.Run("ErrorWhenLoadConfigFails", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.LoadConfigFunc = func() error { + return fmt.Errorf("load config failed") + } + + err := ctx.LoadConfig() + + if err == nil { + t.Error("Expected error when LoadConfig fails") + } + + if !strings.Contains(err.Error(), "load config failed") { + t.Errorf("Expected error about load config failed, got: %v", err) + } + }) +} + +func TestExecutionContext_HandleSessionReset(t *testing.T) { + t.Run("ResetsWhenNoSessionToken", func(t *testing.T) { + t.Cleanup(func() { + os.Unsetenv("NO_CACHE") + os.Unsetenv("WINDSOR_SESSION_TOKEN") + }) + os.Unsetenv("WINDSOR_SESSION_TOKEN") + + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + mockShell := mocks.Shell.(*shell.MockShell) + resetCalled := false + mockShell.ResetFunc = func(clearSession ...bool) { + resetCalled = true + } + mockShell.CheckResetFlagsFunc = func() (bool, error) { + return false, nil + } + + err := ctx.HandleSessionReset() + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if !resetCalled { + t.Error("Expected Reset to be called when no session token") + } + }) + + t.Run("ResetsWhenResetFlagSet", func(t *testing.T) { + t.Cleanup(func() { + os.Unsetenv("NO_CACHE") + }) + + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + mockShell := mocks.Shell.(*shell.MockShell) + resetCalled := false + mockShell.ResetFunc = func(clearSession ...bool) { + resetCalled = true + } + mockShell.CheckResetFlagsFunc = func() (bool, error) { + return true, nil + } + + err := ctx.HandleSessionReset() + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if !resetCalled { + t.Error("Expected Reset to be called when reset flag set") + } + }) + + t.Run("SkipsResetWhenSessionTokenAndNoResetFlag", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + t.Setenv("WINDSOR_SESSION_TOKEN", "test-token") + + mockShell := mocks.Shell.(*shell.MockShell) + resetCalled := false + mockShell.ResetFunc = func(clearSession ...bool) { + resetCalled = true + } + mockShell.CheckResetFlagsFunc = func() (bool, error) { + return false, nil + } + + err := ctx.HandleSessionReset() + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if resetCalled { + t.Error("Expected Reset not to be called when session token exists and no reset flag") + } + }) + + t.Run("ErrorWhenShellNotInitialized", func(t *testing.T) { + ctx := &ExecutionContext{} + + err := ctx.HandleSessionReset() + + if err == nil { + t.Error("Expected error when Shell is nil") + } + + if !strings.Contains(err.Error(), "shell not initialized") { + t.Errorf("Expected error about shell not initialized, got: %v", err) + } + }) + + t.Run("ErrorWhenCheckResetFlagsFails", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + mockShell := mocks.Shell.(*shell.MockShell) + mockShell.CheckResetFlagsFunc = func() (bool, error) { + return false, fmt.Errorf("check reset flags failed") + } + + err := ctx.HandleSessionReset() + + if err == nil { + t.Error("Expected error when CheckResetFlags fails") + } + + if !strings.Contains(err.Error(), "failed to check reset flags") { + t.Errorf("Expected error about check reset flags, got: %v", err) + } + }) +} + +func TestExecutionContext_ApplyConfigDefaults(t *testing.T) { + t.Run("SkipsWhenConfigAlreadyLoaded", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.IsLoadedFunc = func() bool { + return true + } + + err := ctx.ApplyConfigDefaults() + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + }) + + t.Run("SetsDefaultsForDevMode", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + ctx.ContextName = "local" + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.IsLoadedFunc = func() bool { + return false + } + mockConfigHandler.IsDevModeFunc = func(contextName string) bool { + return true + } + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + return "" + } + + setDefaultCalled := false + mockConfigHandler.SetDefaultFunc = func(cfg v1alpha1.Context) error { + setDefaultCalled = true + return nil + } + + setCalls := make(map[string]interface{}) + mockConfigHandler.SetFunc = func(key string, value interface{}) error { + setCalls[key] = value + return nil + } + + err := ctx.ApplyConfigDefaults() + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if !setDefaultCalled { + t.Error("Expected SetDefault to be called") + } + + if setCalls["dev"] != true { + t.Error("Expected dev to be set to true") + } + + if setCalls["provider"] != "generic" { + t.Error("Expected provider to be set to generic") + } + }) + + t.Run("ErrorWhenConfigHandlerNotAvailable", func(t *testing.T) { + ctx := &ExecutionContext{} + + err := ctx.ApplyConfigDefaults() + + if err == nil { + t.Error("Expected error when ConfigHandler is nil") + } + + if !strings.Contains(err.Error(), "config handler not available") { + t.Errorf("Expected error about config handler not available, got: %v", err) + } + }) + + t.Run("ErrorWhenSetDevFails", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + ctx.ContextName = "local" + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.IsLoadedFunc = func() bool { + return false + } + mockConfigHandler.IsDevModeFunc = func(contextName string) bool { + return true + } + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + return "" + } + mockConfigHandler.SetFunc = func(key string, value interface{}) error { + if key == "dev" { + return fmt.Errorf("set dev failed") + } + return nil + } + + err := ctx.ApplyConfigDefaults() + + if err == nil { + t.Error("Expected error when Set dev fails") + } + + if !strings.Contains(err.Error(), "failed to set dev mode") { + t.Errorf("Expected error about set dev mode, got: %v", err) + } + }) + + t.Run("SetsDefaultsForNonDevMode", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + ctx.ContextName = "prod" + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.IsLoadedFunc = func() bool { + return false + } + mockConfigHandler.IsDevModeFunc = func(contextName string) bool { + return false + } + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + return "" + } + + setDefaultCalled := false + mockConfigHandler.SetDefaultFunc = func(cfg v1alpha1.Context) error { + setDefaultCalled = true + return nil + } + + mockConfigHandler.SetFunc = func(key string, value interface{}) error { + return nil + } + + err := ctx.ApplyConfigDefaults() + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if !setDefaultCalled { + t.Error("Expected SetDefault to be called") + } + }) + + t.Run("SetsVMDriverForDockerDesktop", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + ctx.ContextName = "local" + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.IsLoadedFunc = func() bool { + return false + } + mockConfigHandler.IsDevModeFunc = func(contextName string) bool { + return true + } + + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + return "" + } + + mockConfigHandler.SetDefaultFunc = func(cfg v1alpha1.Context) error { + return nil + } + + setCalls := make(map[string]interface{}) + mockConfigHandler.SetFunc = func(key string, value interface{}) error { + setCalls[key] = value + return nil + } + + err := ctx.ApplyConfigDefaults() + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + vmDriverSet := false + for key := range setCalls { + if key == "vm.driver" { + vmDriverSet = true + break + } + } + + if !vmDriverSet { + t.Error("Expected vm.driver to be set") + } + }) + + t.Run("ErrorWhenSetDefaultFails", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + ctx.ContextName = "local" + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.IsLoadedFunc = func() bool { + return false + } + mockConfigHandler.IsDevModeFunc = func(contextName string) bool { + return true + } + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + return "" + } + + mockConfigHandler.SetDefaultFunc = func(cfg v1alpha1.Context) error { + return fmt.Errorf("set default failed") + } + + mockConfigHandler.SetFunc = func(key string, value interface{}) error { + return nil + } + + err := ctx.ApplyConfigDefaults() + + if err == nil { + t.Error("Expected error when SetDefault fails") + } + + if !strings.Contains(err.Error(), "failed to set default config") { + t.Errorf("Expected error about set default config, got: %v", err) + } + }) + + t.Run("ErrorWhenSetVMDriverFails", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + ctx.ContextName = "local" + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.IsLoadedFunc = func() bool { + return false + } + mockConfigHandler.IsDevModeFunc = func(contextName string) bool { + return true + } + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + return "" + } + + mockConfigHandler.SetDefaultFunc = func(cfg v1alpha1.Context) error { + return nil + } + + mockConfigHandler.SetFunc = func(key string, value interface{}) error { + if key == "vm.driver" { + return fmt.Errorf("set vm.driver failed") + } + return nil + } + + err := ctx.ApplyConfigDefaults() + + if err == nil { + t.Error("Expected error when Set vm.driver fails") + } + + if !strings.Contains(err.Error(), "failed to set vm.driver") { + t.Errorf("Expected error about set vm.driver, got: %v", err) + } + }) + + t.Run("ErrorWhenSetProviderFails", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + ctx.ContextName = "local" + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.IsLoadedFunc = func() bool { + return false + } + mockConfigHandler.IsDevModeFunc = func(contextName string) bool { + return true + } + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + return "" + } + + mockConfigHandler.SetDefaultFunc = func(cfg v1alpha1.Context) error { + return nil + } + + mockConfigHandler.SetFunc = func(key string, value interface{}) error { + if key == "provider" { + return fmt.Errorf("set provider failed") + } + return nil + } + + err := ctx.ApplyConfigDefaults() + + if err == nil { + t.Error("Expected error when Set provider fails") + } + + if !strings.Contains(err.Error(), "failed to set provider") { + t.Errorf("Expected error about set provider, got: %v", err) + } + }) +} + +func TestExecutionContext_ApplyProviderDefaults(t *testing.T) { + t.Run("SetsAWSDefaults", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + ctx.ContextName = "prod" + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + setCalls := make(map[string]interface{}) + mockConfigHandler.SetFunc = func(key string, value interface{}) error { + setCalls[key] = value + return nil + } + + err := ctx.ApplyProviderDefaults("aws") + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if setCalls["aws.enabled"] != true { + t.Error("Expected aws.enabled to be set to true") + } + + if setCalls["cluster.driver"] != "eks" { + t.Error("Expected cluster.driver to be set to eks") + } + }) + + t.Run("SetsAzureDefaults", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + ctx.ContextName = "prod" + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + setCalls := make(map[string]interface{}) + mockConfigHandler.SetFunc = func(key string, value interface{}) error { + setCalls[key] = value + return nil + } + + err := ctx.ApplyProviderDefaults("azure") + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if setCalls["azure.enabled"] != true { + t.Error("Expected azure.enabled to be set to true") + } + + if setCalls["cluster.driver"] != "aks" { + t.Error("Expected cluster.driver to be set to aks") + } + }) + + t.Run("SetsGenericDefaults", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + ctx.ContextName = "local" + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + setCalls := make(map[string]interface{}) + mockConfigHandler.SetFunc = func(key string, value interface{}) error { + setCalls[key] = value + return nil + } + + err := ctx.ApplyProviderDefaults("generic") + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if setCalls["cluster.driver"] != "talos" { + t.Error("Expected cluster.driver to be set to talos") + } + }) + + t.Run("ErrorWhenConfigHandlerNotAvailable", func(t *testing.T) { + ctx := &ExecutionContext{} + + err := ctx.ApplyProviderDefaults("aws") + + if err == nil { + t.Error("Expected error when ConfigHandler is nil") + } + + if !strings.Contains(err.Error(), "config handler not available") { + t.Errorf("Expected error about config handler not available, got: %v", err) + } + }) + + t.Run("ErrorWhenSetFails", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + ctx.ContextName = "prod" + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.SetFunc = func(key string, value interface{}) error { + if key == "aws.enabled" { + return fmt.Errorf("set aws.enabled failed") + } + return nil + } + + err := ctx.ApplyProviderDefaults("aws") + + if err == nil { + t.Error("Expected error when Set fails") + } + + if !strings.Contains(err.Error(), "failed to set aws.enabled") { + t.Errorf("Expected error about set aws.enabled, got: %v", err) + } + }) + + t.Run("SetsDefaultsForDevModeWithNoProvider", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + ctx.ContextName = "local" + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "provider" { + return "" + } + if key == "cluster.driver" { + return "" + } + return "" + } + mockConfigHandler.IsDevModeFunc = func(contextName string) bool { + return true + } + + setCalls := make(map[string]interface{}) + mockConfigHandler.SetFunc = func(key string, value interface{}) error { + setCalls[key] = value + return nil + } + + err := ctx.ApplyProviderDefaults("") + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if setCalls["cluster.driver"] != "talos" { + t.Error("Expected cluster.driver to be set to talos for dev mode") + } + }) + + t.Run("GetsProviderFromConfig", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + ctx.ContextName = "prod" + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "provider" { + return "aws" + } + return "" + } + + setCalls := make(map[string]interface{}) + mockConfigHandler.SetFunc = func(key string, value interface{}) error { + setCalls[key] = value + return nil + } + + err := ctx.ApplyProviderDefaults("") + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if setCalls["aws.enabled"] != true { + t.Error("Expected aws.enabled to be set to true") + } + }) + + t.Run("ErrorWhenSetClusterDriverFails", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + ctx.ContextName = "prod" + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + return "" + } + mockConfigHandler.IsDevModeFunc = func(contextName string) bool { + return false + } + mockConfigHandler.SetFunc = func(key string, value interface{}) error { + if key == "cluster.driver" { + return fmt.Errorf("set cluster.driver failed") + } + return nil + } + + err := ctx.ApplyProviderDefaults("generic") + + if err == nil { + t.Error("Expected error when Set cluster.driver fails") + } + + if !strings.Contains(err.Error(), "failed to set cluster.driver") { + t.Errorf("Expected error about set cluster.driver, got: %v", err) + } + }) + + t.Run("ErrorWhenSetAzureDriverFails", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + ctx.ContextName = "prod" + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.SetFunc = func(key string, value interface{}) error { + if key == "cluster.driver" { + return fmt.Errorf("set cluster.driver failed") + } + return nil + } + + err := ctx.ApplyProviderDefaults("azure") + + if err == nil { + t.Error("Expected error when Set cluster.driver fails for azure") + } + + if !strings.Contains(err.Error(), "failed to set cluster.driver") { + t.Errorf("Expected error about set cluster.driver, got: %v", err) + } + }) + + t.Run("ErrorWhenSetDevModeClusterDriverFails", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + ctx.ContextName = "local" + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "provider" { + return "" + } + if key == "cluster.driver" { + return "" + } + return "" + } + mockConfigHandler.IsDevModeFunc = func(contextName string) bool { + return true + } + mockConfigHandler.SetFunc = func(key string, value interface{}) error { + if key == "cluster.driver" { + return fmt.Errorf("set cluster.driver failed") + } + return nil + } + + err := ctx.ApplyProviderDefaults("") + + if err == nil { + t.Error("Expected error when Set cluster.driver fails for dev mode") + } + + if !strings.Contains(err.Error(), "failed to set cluster.driver") { + t.Errorf("Expected error about set cluster.driver, got: %v", err) + } + }) +} + +func TestExecutionContext_PrepareTools(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + mockToolsManager := &MockToolsManager{} + mockToolsManager.CheckFunc = func() error { + return nil + } + mockToolsManager.InstallFunc = func() error { + return nil + } + ctx.ToolsManager = mockToolsManager + + err := ctx.PrepareTools() + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + }) + + t.Run("ErrorWhenCheckFails", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + mockToolsManager := &MockToolsManager{} + mockToolsManager.CheckFunc = func() error { + return fmt.Errorf("tools check failed") + } + ctx.ToolsManager = mockToolsManager + + err := ctx.PrepareTools() + + if err == nil { + t.Error("Expected error when Check fails") + } + + if !strings.Contains(err.Error(), "error checking tools") { + t.Errorf("Expected error about checking tools, got: %v", err) + } + }) + + t.Run("ErrorWhenInstallFails", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + mockToolsManager := &MockToolsManager{} + mockToolsManager.CheckFunc = func() error { + return nil + } + mockToolsManager.InstallFunc = func() error { + return fmt.Errorf("tools install failed") + } + ctx.ToolsManager = mockToolsManager + + err := ctx.PrepareTools() + + if err == nil { + t.Error("Expected error when Install fails") + } + + if !strings.Contains(err.Error(), "error installing tools") { + t.Errorf("Expected error about installing tools, got: %v", err) + } + }) +} + +func TestExecutionContext_GetBuildID(t *testing.T) { + t.Run("CreatesNewBuildIDWhenNoneExists", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + tmpDir := t.TempDir() + ctx.ProjectRoot = tmpDir + + buildID, err := ctx.GetBuildID() + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if buildID == "" { + t.Error("Expected build ID to be generated") + } + }) + + t.Run("ReturnsExistingBuildID", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + tmpDir := t.TempDir() + ctx.ProjectRoot = tmpDir + + buildID1, err := ctx.GetBuildID() + if err != nil { + t.Fatalf("Failed to get initial build ID: %v", err) + } + + buildID2, err := ctx.GetBuildID() + if err != nil { + t.Fatalf("Failed to get second build ID: %v", err) + } + + if buildID1 != buildID2 { + t.Errorf("Expected build IDs to match, got %s and %s", buildID1, buildID2) + } + }) +} + +func TestExecutionContext_GenerateBuildID(t *testing.T) { + t.Run("GeneratesAndSavesBuildID", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + tmpDir := t.TempDir() + ctx.ProjectRoot = tmpDir + + buildID, err := ctx.GenerateBuildID() + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if buildID == "" { + t.Error("Expected build ID to be generated") + } + }) + + t.Run("IncrementsBuildIDOnSubsequentCalls", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + tmpDir := t.TempDir() + ctx.ProjectRoot = tmpDir + + buildID1, err := ctx.GenerateBuildID() + if err != nil { + t.Fatalf("Failed to generate first build ID: %v", err) + } + + buildID2, err := ctx.GenerateBuildID() + if err != nil { + t.Fatalf("Failed to generate second build ID: %v", err) + } + + if buildID1 == buildID2 { + t.Error("Expected build IDs to be different") + } + }) + + t.Run("ErrorOnInvalidFormat", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + tmpDir := t.TempDir() + ctx.ProjectRoot = tmpDir + + buildID, err := ctx.incrementBuildID("invalid", "251112") + + if err == nil { + t.Error("Expected error for invalid format") + } + + if buildID != "" { + t.Error("Expected empty build ID on error") + } + }) + + t.Run("ErrorOnInvalidCounter", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + tmpDir := t.TempDir() + ctx.ProjectRoot = tmpDir + + buildID, err := ctx.incrementBuildID("251112.123.abc", "251112") + + if err == nil { + t.Error("Expected error for invalid counter") + } + + if buildID != "" { + t.Error("Expected empty build ID on error") + } + }) + + t.Run("ResetsCounterOnDateChange", func(t *testing.T) { + mocks := setupEnvironmentMocks(t) + ctx := mocks.ExecutionContext + + tmpDir := t.TempDir() + ctx.ProjectRoot = tmpDir + + buildID, err := ctx.incrementBuildID("251111.123.5", "251112") + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if !strings.HasPrefix(buildID, "251112.") { + t.Error("Expected new date in build ID") + } + + if !strings.HasSuffix(buildID, ".1") { + t.Error("Expected counter to reset to 1") + } + }) +} diff --git a/pkg/project/project.go b/pkg/project/project.go new file mode 100644 index 000000000..60b335405 --- /dev/null +++ b/pkg/project/project.go @@ -0,0 +1,165 @@ +package project + +import ( + "fmt" + "path/filepath" + + "github.com/windsorcli/cli/pkg/composer" + "github.com/windsorcli/cli/pkg/context" + "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/provisioner" + "github.com/windsorcli/cli/pkg/workstation" +) + +// Project orchestrates the setup and initialization of a Windsor project. +// It coordinates context, provisioner, composer, and workstation managers +// to provide a unified interface for project initialization and management. +type Project struct { + Context *context.ExecutionContext + Provisioner *provisioner.Provisioner + Composer *composer.Composer + Workstation *workstation.Workstation +} + +// NewProject creates and initializes a new Project instance with all required managers. +// It sets up the execution context, applies config defaults, and creates provisioner, +// composer, and workstation managers. The workstation is only created if the project +// is in dev mode. If an existing context is provided, it will be reused; otherwise, +// a new context will be created. Returns the initialized Project or an error if any step fails. +// After creation, call Configure() to apply flag overrides if needed. +func NewProject(injector di.Injector, contextName string, existingCtx ...*context.ExecutionContext) (*Project, error) { + var baseCtx *context.ExecutionContext + var err error + + if len(existingCtx) > 0 && existingCtx[0] != nil { + baseCtx = existingCtx[0] + } else { + baseCtx = &context.ExecutionContext{ + Injector: injector, + } + baseCtx, err = context.NewContext(baseCtx) + if err != nil { + return nil, fmt.Errorf("failed to initialize context: %w", err) + } + } + + if contextName == "" { + contextName = baseCtx.ConfigHandler.GetContext() + if contextName == "" { + contextName = "local" + } + } + + baseCtx.ContextName = contextName + baseCtx.ConfigRoot = filepath.Join(baseCtx.ProjectRoot, "contexts", contextName) + + if err := baseCtx.ApplyConfigDefaults(); err != nil { + return nil, err + } + + provCtx := &provisioner.ProvisionerExecutionContext{ + ExecutionContext: *baseCtx, + } + prov := provisioner.NewProvisioner(provCtx) + + composerCtx := &composer.ComposerExecutionContext{ + ExecutionContext: *baseCtx, + } + comp := composer.NewComposer(composerCtx) + + var ws *workstation.Workstation + if baseCtx.ConfigHandler.IsDevMode(baseCtx.ContextName) { + workstationCtx := &workstation.WorkstationExecutionContext{ + ExecutionContext: *baseCtx, + } + ws, err = workstation.NewWorkstation(workstationCtx, baseCtx.Injector) + if err != nil { + return nil, fmt.Errorf("failed to create workstation: %w", err) + } + } + + return &Project{ + Context: baseCtx, + Provisioner: prov, + Composer: comp, + Workstation: ws, + }, nil +} + +// Configure loads configuration from disk and applies flag-based overrides. +// This should be called after NewProject if command flags need to override +// configuration values. Returns an error if loading or applying overrides fails. +func (p *Project) Configure(flagOverrides map[string]any) error { + contextName := p.Context.ContextName + if contextName == "" { + contextName = "local" + } + + if p.Context.ConfigHandler.IsDevMode(contextName) { + if flagOverrides == nil { + flagOverrides = make(map[string]any) + } + if _, exists := flagOverrides["provider"]; !exists { + if p.Context.ConfigHandler.GetString("provider") == "" { + flagOverrides["provider"] = "generic" + } + } + } + + providerOverride := "" + if flagOverrides != nil { + if prov, ok := flagOverrides["provider"].(string); ok { + providerOverride = prov + } + } + + if err := p.Context.ApplyProviderDefaults(providerOverride); err != nil { + return err + } + + if err := p.Context.ConfigHandler.LoadConfig(); err != nil { + return fmt.Errorf("failed to load config: %w", err) + } + + for key, value := range flagOverrides { + if err := p.Context.ConfigHandler.Set(key, value); err != nil { + return fmt.Errorf("failed to set %s: %w", key, err) + } + } + + return nil +} + +// Initialize runs the complete initialization sequence for the project. +// It initializes network, prepares context, generates infrastructure, prepares tools, +// and bootstraps the environment. The overwrite parameter controls whether +// infrastructure generation should overwrite existing files. Returns an error +// if any step fails. +func (p *Project) Initialize(overwrite bool) error { + if p.Workstation != nil && p.Workstation.NetworkManager != nil { + if err := p.Workstation.NetworkManager.Initialize(); err != nil { + return fmt.Errorf("failed to initialize network manager: %w", err) + } + } + + if err := p.Context.ConfigHandler.GenerateContextID(); err != nil { + return fmt.Errorf("failed to generate context ID: %w", err) + } + if err := p.Context.ConfigHandler.SaveConfig(overwrite); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + if err := p.Composer.Generate(overwrite); err != nil { + return fmt.Errorf("failed to generate infrastructure: %w", err) + } + + if err := p.Context.PrepareTools(); err != nil { + return err + } + + if err := p.Context.LoadEnvironment(true); err != nil { + return fmt.Errorf("failed to load environment: %w", err) + } + + return nil +} diff --git a/pkg/project/project_test.go b/pkg/project/project_test.go new file mode 100644 index 000000000..0cf1cdc6b --- /dev/null +++ b/pkg/project/project_test.go @@ -0,0 +1,571 @@ +package project + +import ( + "fmt" + "path/filepath" + "strings" + "testing" + + "github.com/windsorcli/cli/pkg/composer" + "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/context/tools" + "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/provisioner" + "github.com/windsorcli/cli/pkg/workstation" +) + +// ============================================================================= +// Test Setup +// ============================================================================= + +type Mocks struct { + Injector di.Injector + ConfigHandler config.ConfigHandler + Shell shell.Shell + Workstation *workstation.Workstation + Composer *composer.Composer + Provisioner *provisioner.Provisioner +} + +func setupMocks(t *testing.T) *Mocks { + t.Helper() + + tmpDir := t.TempDir() + configRoot := filepath.Join(tmpDir, "contexts", "test-context") + + injector := di.NewInjector() + configHandler := config.NewMockConfigHandler() + mockShell := shell.NewMockShell() + + configHandler.GetStringFunc = func(key string, defaultValue ...string) string { + switch key { + case "cluster.driver": + return "talos" + case "provider": + return "" + default: + if len(defaultValue) > 0 { + return defaultValue[0] + } + return "" + } + } + + configHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + return false + } + + configHandler.GetContextFunc = func() string { + return "test-context" + } + + configHandler.IsDevModeFunc = func(contextName string) bool { + return false + } + + configHandler.LoadConfigFunc = func() error { + return nil + } + + configHandler.SetFunc = func(key string, value any) error { + return nil + } + + configHandler.GenerateContextIDFunc = func() error { + return nil + } + + configHandler.SaveConfigFunc = func(hasSetFlags ...bool) error { + return nil + } + + mockShell.GetProjectRootFunc = func() (string, error) { + return tmpDir, nil + } + + mockShell.GetSessionTokenFunc = func() (string, error) { + return "test-session-token", nil + } + + configHandler.GetConfigRootFunc = func() (string, error) { + return configRoot, nil + } + + mockToolsManager := tools.NewMockToolsManager() + mockToolsManager.CheckFunc = func() error { + return nil + } + + injector.Register("shell", mockShell) + injector.Register("configHandler", configHandler) + injector.Register("toolsManager", mockToolsManager) + + baseCtx := &context.ExecutionContext{ + Injector: injector, + } + + ctx, err := context.NewContext(baseCtx) + if err != nil { + t.Fatalf("Failed to create context: %v", err) + } + + provCtx := &provisioner.ProvisionerExecutionContext{ + ExecutionContext: *ctx, + } + prov := provisioner.NewProvisioner(provCtx) + + composerCtx := &composer.ComposerExecutionContext{ + ExecutionContext: *ctx, + } + comp := composer.NewComposer(composerCtx) + + return &Mocks{ + Injector: injector, + ConfigHandler: configHandler, + Shell: mockShell, + Provisioner: prov, + Composer: comp, + } +} + +// ============================================================================= +// Test Constructor +// ============================================================================= + +func TestNewProject(t *testing.T) { + t.Run("CreatesProjectWithDependencies", func(t *testing.T) { + mocks := setupMocks(t) + + proj, err := NewProject(mocks.Injector, "test-context") + + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if proj == nil { + t.Fatal("Expected Project to be created") + } + + if proj.Context == nil { + t.Error("Expected Context to be set") + } + + if proj.Provisioner == nil { + t.Error("Expected Provisioner to be set") + } + + if proj.Composer == nil { + t.Error("Expected Composer to be set") + } + + if proj.Context.ContextName != "test-context" { + t.Errorf("Expected ContextName to be 'test-context', got: %s", proj.Context.ContextName) + } + }) + + t.Run("UsesProvidedContextName", func(t *testing.T) { + mocks := setupMocks(t) + + proj, err := NewProject(mocks.Injector, "custom-context") + + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if proj.Context.ContextName != "custom-context" { + t.Errorf("Expected ContextName to be 'custom-context', got: %s", proj.Context.ContextName) + } + }) + + t.Run("UsesConfigContextWhenContextNameEmpty", func(t *testing.T) { + mocks := setupMocks(t) + + proj, err := NewProject(mocks.Injector, "") + + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if proj.Context.ContextName != "test-context" { + t.Errorf("Expected ContextName to be 'test-context', got: %s", proj.Context.ContextName) + } + }) + + t.Run("UsesLocalWhenContextNameAndConfigContextEmpty", func(t *testing.T) { + mocks := setupMocks(t) + mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfig.GetContextFunc = func() string { + return "" + } + + proj, err := NewProject(mocks.Injector, "") + + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if proj.Context.ContextName != "local" { + t.Errorf("Expected ContextName to be 'local', got: %s", proj.Context.ContextName) + } + }) + + t.Run("CreatesWorkstationWhenDevMode", func(t *testing.T) { + mocks := setupMocks(t) + mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfig.IsDevModeFunc = func(contextName string) bool { + return true + } + + proj, err := NewProject(mocks.Injector, "test-context") + + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if proj.Workstation == nil { + t.Error("Expected Workstation to be created in dev mode") + } + }) + + t.Run("SkipsWorkstationWhenNotDevMode", func(t *testing.T) { + mocks := setupMocks(t) + mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfig.IsDevModeFunc = func(contextName string) bool { + return false + } + + proj, err := NewProject(mocks.Injector, "test-context") + + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if proj.Workstation != nil { + t.Error("Expected Workstation to be nil when not in dev mode") + } + }) + + t.Run("ErrorOnContextInitializationFailure", func(t *testing.T) { + var injector di.Injector + + proj, err := NewProject(injector, "test-context") + + if err == nil { + t.Error("Expected error for context initialization failure") + return + } + + if proj != nil { + t.Error("Expected Project to be nil on error") + } + + if !strings.Contains(err.Error(), "failed to initialize context") { + t.Errorf("Expected specific error message, got: %v", err) + } + }) + +} + +// ============================================================================= +// Test Public Methods +// ============================================================================= + +func TestProject_Configure(t *testing.T) { + t.Run("SuccessWithNilFlagOverrides", func(t *testing.T) { + mocks := setupMocks(t) + proj, err := NewProject(mocks.Injector, "test-context") + if err != nil { + t.Fatalf("Failed to create project: %v", err) + } + + err = proj.Configure(nil) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + }) + + t.Run("SuccessWithEmptyFlagOverrides", func(t *testing.T) { + mocks := setupMocks(t) + proj, err := NewProject(mocks.Injector, "test-context") + if err != nil { + t.Fatalf("Failed to create project: %v", err) + } + + err = proj.Configure(make(map[string]any)) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + }) + + t.Run("SuccessWithFlagOverrides", func(t *testing.T) { + mocks := setupMocks(t) + proj, err := NewProject(mocks.Injector, "test-context") + if err != nil { + t.Fatalf("Failed to create project: %v", err) + } + + flagOverrides := map[string]any{ + "provider": "aws", + "key": "value", + } + + err = proj.Configure(flagOverrides) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + }) + + t.Run("SetsGenericProviderInDevModeWhenProviderNotSet", func(t *testing.T) { + mocks := setupMocks(t) + mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfig.IsDevModeFunc = func(contextName string) bool { + return true + } + mockConfig.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "provider" { + return "" + } + return "" + } + + proj, err := NewProject(mocks.Injector, "test-context") + if err != nil { + t.Fatalf("Failed to create project: %v", err) + } + + providerSet := false + mockConfig.SetFunc = func(key string, value any) error { + if key == "provider" && value == "generic" { + providerSet = true + } + return nil + } + + err = proj.Configure(nil) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if !providerSet { + t.Error("Expected provider to be set to 'generic' in dev mode") + } + }) + + t.Run("SkipsGenericProviderWhenProviderAlreadySet", func(t *testing.T) { + mocks := setupMocks(t) + mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfig.IsDevModeFunc = func(contextName string) bool { + return true + } + mockConfig.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "provider" { + return "aws" + } + return "" + } + + proj, err := NewProject(mocks.Injector, "test-context") + if err != nil { + t.Fatalf("Failed to create project: %v", err) + } + + genericSet := false + mockConfig.SetFunc = func(key string, value any) error { + if key == "provider" && value == "generic" { + genericSet = true + } + return nil + } + + err = proj.Configure(nil) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if genericSet { + t.Error("Expected provider not to be set to 'generic' when already set") + } + }) + + t.Run("ErrorOnApplyProviderDefaultsFailure", func(t *testing.T) { + mocks := setupMocks(t) + proj, err := NewProject(mocks.Injector, "test-context") + if err != nil { + t.Fatalf("Failed to create project: %v", err) + } + + mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfig.SetFunc = func(key string, value any) error { + if key == "cluster.driver" { + return fmt.Errorf("set cluster.driver failed") + } + return nil + } + + err = proj.Configure(map[string]any{"provider": "aws"}) + + if err == nil { + t.Error("Expected error for ApplyProviderDefaults failure") + return + } + }) + + t.Run("ErrorOnLoadConfigFailure", func(t *testing.T) { + mocks := setupMocks(t) + proj, err := NewProject(mocks.Injector, "test-context") + if err != nil { + t.Fatalf("Failed to create project: %v", err) + } + + mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfig.LoadConfigFunc = func() error { + return fmt.Errorf("load config failed") + } + + err = proj.Configure(nil) + + if err == nil { + t.Error("Expected error for LoadConfig failure") + return + } + + if !strings.Contains(err.Error(), "failed to load config") { + t.Errorf("Expected specific error message, got: %v", err) + } + }) + + t.Run("ErrorOnSetFailure", func(t *testing.T) { + mocks := setupMocks(t) + proj, err := NewProject(mocks.Injector, "test-context") + if err != nil { + t.Fatalf("Failed to create project: %v", err) + } + + mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfig.SetFunc = func(key string, value any) error { + return fmt.Errorf("set failed") + } + + err = proj.Configure(map[string]any{"key": "value"}) + + if err == nil { + t.Error("Expected error for Set failure") + return + } + + if !strings.Contains(err.Error(), "failed to set") { + t.Errorf("Expected specific error message, got: %v", err) + } + }) +} + +func TestProject_Initialize(t *testing.T) { + t.Run("SuccessWithoutWorkstation", func(t *testing.T) { + mocks := setupMocks(t) + proj, err := NewProject(mocks.Injector, "test-context") + if err != nil { + t.Fatalf("Failed to create project: %v", err) + } + + err = proj.Initialize(false) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + }) + + t.Run("SuccessWithWorkstation", func(t *testing.T) { + mocks := setupMocks(t) + mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfig.IsDevModeFunc = func(contextName string) bool { + return true + } + + proj, err := NewProject(mocks.Injector, "test-context") + if err != nil { + t.Fatalf("Failed to create project: %v", err) + } + + if proj.Workstation == nil { + t.Fatal("Expected workstation to be created") + } + + proj.Workstation.NetworkManager = nil + + err = proj.Initialize(false) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + }) + + t.Run("SuccessWithOverwriteTrue", func(t *testing.T) { + mocks := setupMocks(t) + proj, err := NewProject(mocks.Injector, "test-context") + if err != nil { + t.Fatalf("Failed to create project: %v", err) + } + + err = proj.Initialize(true) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + }) + + t.Run("ErrorOnGenerateContextIDFailure", func(t *testing.T) { + mocks := setupMocks(t) + proj, err := NewProject(mocks.Injector, "test-context") + if err != nil { + t.Fatalf("Failed to create project: %v", err) + } + + mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfig.GenerateContextIDFunc = func() error { + return fmt.Errorf("generate context ID failed") + } + + err = proj.Initialize(false) + + if err == nil { + t.Error("Expected error for GenerateContextID failure") + return + } + + if !strings.Contains(err.Error(), "failed to generate context ID") { + t.Errorf("Expected specific error message, got: %v", err) + } + }) + + t.Run("ErrorOnSaveConfigFailure", func(t *testing.T) { + mocks := setupMocks(t) + proj, err := NewProject(mocks.Injector, "test-context") + if err != nil { + t.Fatalf("Failed to create project: %v", err) + } + + mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfig.SaveConfigFunc = func(hasSetFlags ...bool) error { + return fmt.Errorf("save config failed") + } + + err = proj.Initialize(false) + + if err == nil { + t.Error("Expected error for SaveConfig failure") + return + } + + if !strings.Contains(err.Error(), "failed to save config") { + t.Errorf("Expected specific error message, got: %v", err) + } + }) + +} diff --git a/pkg/provisioner/provisioner.go b/pkg/provisioner/provisioner.go index 0e429e047..a22640f3a 100644 --- a/pkg/provisioner/provisioner.go +++ b/pkg/provisioner/provisioner.go @@ -3,8 +3,10 @@ package provisioner import ( "context" "fmt" + "os" "time" + "github.com/briandowns/spinner" blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" "github.com/windsorcli/cli/pkg/constants" execcontext "github.com/windsorcli/cli/pkg/context" @@ -58,8 +60,15 @@ func NewProvisioner(ctx *ProvisionerExecutionContext) *Provisioner { } if infra.TerraformStack == nil { - infra.TerraformStack = terraforminfra.NewWindsorStack(infra.Injector) - infra.Injector.Register("terraformStack", infra.TerraformStack) + if existing := infra.Injector.Resolve("terraformStack"); existing != nil { + if ts, ok := existing.(terraforminfra.Stack); ok { + infra.TerraformStack = ts + } + } + if infra.TerraformStack == nil { + infra.TerraformStack = terraforminfra.NewWindsorStack(infra.Injector) + infra.Injector.Register("terraformStack", infra.TerraformStack) + } } if infra.KubernetesClient == nil { @@ -68,8 +77,15 @@ func NewProvisioner(ctx *ProvisionerExecutionContext) *Provisioner { } if infra.KubernetesManager == nil { - infra.KubernetesManager = kubernetes.NewKubernetesManager(infra.Injector) - infra.Injector.Register("kubernetesManager", infra.KubernetesManager) + if existing := infra.Injector.Resolve("kubernetesManager"); existing != nil { + if km, ok := existing.(kubernetes.KubernetesManager); ok { + infra.KubernetesManager = km + } + } + if infra.KubernetesManager == nil { + infra.KubernetesManager = kubernetes.NewKubernetesManager(infra.Injector) + infra.Injector.Register("kubernetesManager", infra.KubernetesManager) + } } if infra.ClusterClient == nil { @@ -145,10 +161,20 @@ func (i *Provisioner) Install(blueprint *blueprintv1alpha1.Blueprint) error { return fmt.Errorf("failed to initialize kubernetes manager: %w", err) } + message := "📐 Installing blueprint resources" + spin := spinner.New(spinner.CharSets[14], 100*time.Millisecond, spinner.WithColor("green")) + spin.Suffix = " " + message + spin.Start() + if err := i.KubernetesManager.ApplyBlueprint(blueprint, constants.DefaultFluxSystemNamespace); err != nil { + spin.Stop() + fmt.Fprintf(os.Stderr, "\033[31m✗ %s - Failed\033[0m\n", message) return fmt.Errorf("failed to apply blueprint: %w", err) } + spin.Stop() + fmt.Fprintf(os.Stderr, "\033[32m✔\033[0m %s - \033[32mDone\033[0m\n", message) + return nil } diff --git a/pkg/workstation/workstation.go b/pkg/workstation/workstation.go index aa903c7b5..7caab0fc5 100644 --- a/pkg/workstation/workstation.go +++ b/pkg/workstation/workstation.go @@ -130,6 +130,9 @@ func (w *Workstation) Up() error { if w.VirtualMachine == nil { return fmt.Errorf("no virtual machine found") } + if err := w.VirtualMachine.Initialize(); err != nil { + return fmt.Errorf("failed to initialize virtual machine: %w", err) + } if err := w.VirtualMachine.Up(); err != nil { return fmt.Errorf("error running virtual machine Up command: %w", err) } @@ -141,6 +144,12 @@ func (w *Workstation) Up() error { if w.ContainerRuntime == nil { return fmt.Errorf("no container runtime found") } + if err := w.ContainerRuntime.Initialize(); err != nil { + return fmt.Errorf("failed to initialize container runtime: %w", err) + } + if err := w.ContainerRuntime.WriteConfig(); err != nil { + return fmt.Errorf("failed to write container runtime config: %w", err) + } if err := w.ContainerRuntime.Up(); err != nil { return fmt.Errorf("error running container runtime Up command: %w", err) } @@ -148,14 +157,21 @@ func (w *Workstation) Up() error { // Configure networking if w.NetworkManager != nil { - if err := w.NetworkManager.ConfigureHostRoute(); err != nil { - return fmt.Errorf("error configuring host route: %w", err) - } - if err := w.NetworkManager.ConfigureGuest(); err != nil { - return fmt.Errorf("error configuring guest: %w", err) + // Only configure guest and host routes for colima + if vmDriver == "colima" { + if err := w.NetworkManager.ConfigureGuest(); err != nil { + return fmt.Errorf("error configuring guest: %w", err) + } + if err := w.NetworkManager.ConfigureHostRoute(); err != nil { + return fmt.Errorf("error configuring host route: %w", err) + } } - if err := w.NetworkManager.ConfigureDNS(); err != nil { - return fmt.Errorf("error configuring DNS: %w", err) + + // Configure DNS if enabled + if dnsEnabled := w.ConfigHandler.GetBool("dns.enabled"); dnsEnabled { + if err := w.NetworkManager.ConfigureDNS(); err != nil { + return fmt.Errorf("error configuring DNS: %w", err) + } } }