From 72503eb17a2fbdc1b035b310d57ec564d66fad58 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:04:18 -0500 Subject: [PATCH] cleanup: Remove old runtime and pipeline packages The runtime and pipeline packages are no longer part of the architecture. Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/pipelines/build_id.go | 197 -- pkg/pipelines/build_id_test.go | 674 ------- pkg/pipelines/check.go | 315 --- pkg/pipelines/check_test.go | 1182 ----------- pkg/pipelines/down.go | 260 --- pkg/pipelines/down_test.go | 1182 ----------- pkg/pipelines/exec.go | 62 - pkg/pipelines/exec_test.go | 240 --- pkg/pipelines/init.go | 496 ----- pkg/pipelines/init_test.go | 1255 ------------ pkg/pipelines/install.go | 128 -- pkg/pipelines/install_test.go | 640 ------ pkg/pipelines/mock_pipeline.go | 45 - pkg/pipelines/mock_pipeline_test.go | 185 -- pkg/pipelines/pipeline.go | 748 ------- pkg/pipelines/pipeline_test.go | 2893 --------------------------- pkg/pipelines/shims.go | 34 - pkg/pipelines/up.go | 238 --- pkg/pipelines/up_test.go | 769 ------- pkg/runtime/runtime.go | 495 ----- pkg/runtime/runtime_loaders.go | 310 --- pkg/runtime/runtime_loaders_test.go | 857 -------- pkg/runtime/runtime_test.go | 2227 --------------------- pkg/runtime/shims.go | 26 - 24 files changed, 15458 deletions(-) delete mode 100644 pkg/pipelines/build_id.go delete mode 100644 pkg/pipelines/build_id_test.go delete mode 100644 pkg/pipelines/check.go delete mode 100644 pkg/pipelines/check_test.go delete mode 100644 pkg/pipelines/down.go delete mode 100644 pkg/pipelines/down_test.go delete mode 100644 pkg/pipelines/exec.go delete mode 100644 pkg/pipelines/exec_test.go delete mode 100644 pkg/pipelines/init.go delete mode 100644 pkg/pipelines/init_test.go delete mode 100644 pkg/pipelines/install.go delete mode 100644 pkg/pipelines/install_test.go delete mode 100644 pkg/pipelines/mock_pipeline.go delete mode 100644 pkg/pipelines/mock_pipeline_test.go delete mode 100644 pkg/pipelines/pipeline.go delete mode 100644 pkg/pipelines/pipeline_test.go delete mode 100644 pkg/pipelines/shims.go delete mode 100644 pkg/pipelines/up.go delete mode 100644 pkg/pipelines/up_test.go delete mode 100644 pkg/runtime/runtime.go delete mode 100644 pkg/runtime/runtime_loaders.go delete mode 100644 pkg/runtime/runtime_loaders_test.go delete mode 100644 pkg/runtime/runtime_test.go delete mode 100644 pkg/runtime/shims.go diff --git a/pkg/pipelines/build_id.go b/pkg/pipelines/build_id.go deleted file mode 100644 index c6d23cdb9..000000000 --- a/pkg/pipelines/build_id.go +++ /dev/null @@ -1,197 +0,0 @@ -package pipelines - -import ( - "context" - "crypto/rand" - "fmt" - "math/big" - "os" - "path/filepath" - "strings" - "time" - - "strconv" - - "github.com/windsorcli/cli/pkg/di" -) - -// BuildIDPipeline manages build ID operations for Windsor CLI build workflows. -// It provides methods for initializing, generating, retrieving, and persisting build IDs -// used to uniquely identify build executions within a Windsor project. -type BuildIDPipeline struct { - BasePipeline -} - -// NewBuildIDPipeline constructs a new BuildIDPipeline instance with default base pipeline initialization. -func NewBuildIDPipeline() *BuildIDPipeline { - return &BuildIDPipeline{ - BasePipeline: *NewBasePipeline(), - } -} - -// Initialize sets up the BuildIDPipeline by initializing its base pipeline with the provided dependency injector and context. -// Returns an error if base initialization fails. -func (p *BuildIDPipeline) Initialize(injector di.Injector, ctx context.Context) error { - if err := p.BasePipeline.Initialize(injector, ctx); err != nil { - return err - } - return nil -} - -// Execute runs the build ID pipeline logic. If the context contains a "new" flag set to true, -// a new build ID is generated and persisted. Otherwise, the current build ID is retrieved and output. -// Returns an error if any operation fails. -func (p *BuildIDPipeline) Execute(ctx context.Context) error { - new, _ := ctx.Value("new").(bool) - - if new { - return p.generateNewBuildID() - } - - return p.getBuildID() -} - -// getBuildID retrieves the current build ID from the .windsor/.build-id file and outputs it. -// If no build ID exists, a new one is generated, persisted, and output. -// Returns an error if retrieval or persistence fails. -func (p *BuildIDPipeline) getBuildID() error { - projectRoot, err := p.shell.GetProjectRoot() - if err != nil { - return fmt.Errorf("failed to get project root: %w", err) - } - - buildIDPath := filepath.Join(projectRoot, ".windsor", ".build-id") - var buildID string - - if _, err := p.shims.Stat(buildIDPath); os.IsNotExist(err) { - buildID = "" - } else { - data, err := p.shims.ReadFile(buildIDPath) - if err != nil { - return fmt.Errorf("failed to read build ID file: %w", err) - } - buildID = strings.TrimSpace(string(data)) - } - - if buildID == "" { - newBuildID, err := p.generateBuildID() - if err != nil { - return fmt.Errorf("failed to generate build ID: %w", err) - } - if err := p.writeBuildIDToFile(newBuildID); err != nil { - return fmt.Errorf("failed to set build ID: %w", err) - } - fmt.Printf("%s\n", newBuildID) - } else { - fmt.Printf("%s\n", buildID) - } - - return nil -} - -// generateNewBuildID generates a new build ID and persists it to the .windsor/.build-id file, -// overwriting any existing value. Outputs the new build ID. Returns an error if generation or persistence fails. -func (p *BuildIDPipeline) generateNewBuildID() error { - newBuildID, err := p.generateBuildID() - if err != nil { - return fmt.Errorf("failed to generate build ID: %w", err) - } - - if err := p.writeBuildIDToFile(newBuildID); err != nil { - return fmt.Errorf("failed to set build ID: %w", err) - } - - fmt.Printf("%s\n", newBuildID) - return nil -} - -// writeBuildIDToFile writes the provided build ID string to the .windsor/.build-id file in the project root. -// Ensures the .windsor directory exists before writing. Returns an error if directory creation or file write fails. -func (p *BuildIDPipeline) writeBuildIDToFile(buildID string) error { - projectRoot, err := p.shell.GetProjectRoot() - if err != nil { - return fmt.Errorf("failed to get project root: %w", err) - } - - buildIDPath := filepath.Join(projectRoot, ".windsor", ".build-id") - buildIDDir := filepath.Dir(buildIDPath) - - if err := p.shims.MkdirAll(buildIDDir, 0755); err != nil { - return fmt.Errorf("failed to create build ID directory: %w", err) - } - - return p.shims.WriteFile(buildIDPath, []byte(buildID), 0644) -} - -// generateBuildID generates and returns a build ID string in the format YYMMDD.RANDOM.#. -// YYMMDD is the current date (year, month, day), RANDOM is a random three-digit number for collision prevention, -// and # is a sequential counter incremented for each build on the same day. If a build ID already exists for the current day, -// the counter is incremented; otherwise, a new build ID is generated with counter set to 1. Ensures global ordering and uniqueness. -// Returns the build ID string or an error if generation or retrieval fails. -func (p *BuildIDPipeline) generateBuildID() (string, error) { - now := time.Now() - yy := now.Year() % 100 - mm := int(now.Month()) - dd := now.Day() - datePart := fmt.Sprintf("%02d%02d%02d", yy, mm, dd) - - projectRoot, err := p.shell.GetProjectRoot() - if err != nil { - return "", fmt.Errorf("failed to get project root: %w", err) - } - - buildIDPath := filepath.Join(projectRoot, ".windsor", ".build-id") - var existingBuildID string - - if _, err := p.shims.Stat(buildIDPath); os.IsNotExist(err) { - existingBuildID = "" - } else { - data, err := p.shims.ReadFile(buildIDPath) - if err != nil { - return "", fmt.Errorf("failed to read build ID file: %w", err) - } - existingBuildID = strings.TrimSpace(string(data)) - } - - if existingBuildID != "" { - return p.incrementBuildID(existingBuildID, datePart) - } - - random, err := rand.Int(rand.Reader, big.NewInt(1000)) - if err != nil { - return "", fmt.Errorf("failed to generate random number: %w", err) - } - counter := 1 - randomPart := fmt.Sprintf("%03d", random.Int64()) - counterPart := fmt.Sprintf("%d", counter) - - return fmt.Sprintf("%s.%s.%s", datePart, randomPart, counterPart), nil -} - -// incrementBuildID parses an existing build ID and increments its counter component. -// If the date component differs from the current date, generates a new random number and resets the counter to 1. -// Returns the incremented or reset build ID string, or an error if the input format is invalid. -func (p *BuildIDPipeline) incrementBuildID(existingBuildID, currentDate string) (string, error) { - parts := strings.Split(existingBuildID, ".") - if len(parts) != 3 { - return "", fmt.Errorf("invalid build ID format: %s", existingBuildID) - } - - existingDate := parts[0] - existingRandom := parts[1] - existingCounter, err := strconv.Atoi(parts[2]) - if err != nil { - return "", fmt.Errorf("invalid counter component: %s", parts[2]) - } - - if existingDate != currentDate { - random, err := rand.Int(rand.Reader, big.NewInt(1000)) - if err != nil { - return "", fmt.Errorf("failed to generate random number: %w", err) - } - return fmt.Sprintf("%s.%03d.1", currentDate, random.Int64()), nil - } - - existingCounter++ - return fmt.Sprintf("%s.%s.%d", existingDate, existingRandom, existingCounter), nil -} diff --git a/pkg/pipelines/build_id_test.go b/pkg/pipelines/build_id_test.go deleted file mode 100644 index 2732d9f9e..000000000 --- a/pkg/pipelines/build_id_test.go +++ /dev/null @@ -1,674 +0,0 @@ -package pipelines - -import ( - "context" - "fmt" - "os" - "strings" - "testing" - "time" - - "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/context/shell" -) - -// ============================================================================= -// Test Setup -// ============================================================================= - -// buildIDMockFileInfo implements os.FileInfo for testing -type buildIDMockFileInfo struct { - name string - isDir bool -} - -func (m buildIDMockFileInfo) Name() string { return m.name } -func (m buildIDMockFileInfo) Size() int64 { return 0 } -func (m buildIDMockFileInfo) Mode() os.FileMode { return 0644 } -func (m buildIDMockFileInfo) ModTime() time.Time { return time.Time{} } -func (m buildIDMockFileInfo) IsDir() bool { return m.isDir } -func (m buildIDMockFileInfo) Sys() any { return nil } - -type BuildIDMocks struct { - Injector di.Injector - Shell *shell.MockShell - Shims *Shims -} - -func setupBuildIDShims(t *testing.T, buildID string, statError error, readError error) *Shims { - t.Helper() - shims := NewShims() - - shims.Stat = func(name string) (os.FileInfo, error) { - if statError != nil { - return nil, statError - } - if strings.Contains(name, ".build-id") { - return buildIDMockFileInfo{name: ".build-id", isDir: false}, nil - } - return nil, os.ErrNotExist - } - - shims.ReadFile = func(name string) ([]byte, error) { - if readError != nil { - return nil, readError - } - if strings.Contains(name, ".build-id") { - return []byte(buildID), nil - } - return []byte{}, nil - } - - // Mock file system operations to avoid real file I/O - shims.RemoveAll = func(path string) error { - return nil - } - - shims.MkdirAll = func(path string, perm os.FileMode) error { - return nil - } - - shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { - return nil - } - - return shims -} - -func setupBuildIDMocks(t *testing.T, buildID string, statError error, readError error) *BuildIDMocks { - t.Helper() - - // Create mock shell - mockShell := &shell.MockShell{} - mockShell.GetProjectRootFunc = func() (string, error) { - return "/test/project", nil - } - - // Create mock shims - mockShims := setupBuildIDShims(t, buildID, statError, readError) - - // Create mock injector - mockInjector := di.NewInjector() - mockInjector.Register("shell", mockShell) - mockInjector.Register("shims", mockShims) - - return &BuildIDMocks{ - Injector: mockInjector, - Shell: mockShell, - Shims: mockShims, - } -} - -// ============================================================================= -// Test Cases -// ============================================================================= - -func TestBuildIDPipeline_NewBuildIDPipeline(t *testing.T) { - t.Run("CreatesPipelineWithDefaultBase", func(t *testing.T) { - // When creating a new BuildIDPipeline - pipeline := NewBuildIDPipeline() - - // Then it should be properly initialized - if pipeline == nil { - t.Fatal("Expected pipeline to be created") - } - }) -} - -func TestBuildIDPipeline_Initialize(t *testing.T) { - t.Run("Success", func(t *testing.T) { - mocks := setupBuildIDMocks(t, "1234567890", nil, nil) - - // Given a BuildIDPipeline - pipeline := NewBuildIDPipeline() - - // When initializing with valid injector - ctx := context.Background() - err := pipeline.Initialize(mocks.Injector, ctx) - - // Then it should succeed - if err != nil { - t.Fatalf("Expected initialization to succeed, got error: %v", err) - } - }) - - t.Run("BasePipelineError", func(t *testing.T) { - // Given a BuildIDPipeline - pipeline := NewBuildIDPipeline() - - // And an invalid injector (missing required dependencies) - mockInjector := di.NewInjector() - - // When initializing with invalid injector - ctx := context.Background() - err := pipeline.Initialize(mockInjector, ctx) - - // Then it should return an error (or succeed if base pipeline handles missing dependencies gracefully) - if err != nil { - // Error is expected - t.Logf("Initialization failed as expected: %v", err) - } else { - // Success is also acceptable if base pipeline handles missing dependencies gracefully - t.Logf("Initialization succeeded (base pipeline may handle missing dependencies gracefully)") - } - }) -} - -func TestBuildIDPipeline_Execute(t *testing.T) { - t.Run("GetExistingBuildID", func(t *testing.T) { - mocks := setupBuildIDMocks(t, "250802.123.5", nil, nil) - - // Given a BuildIDPipeline with existing build ID - pipeline := NewBuildIDPipeline() - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When executing without new flag - err := pipeline.Execute(context.Background()) - - // Then it should succeed and output the existing build ID - if err != nil { - t.Fatalf("Expected execution to succeed, got error: %v", err) - } - }) - - t.Run("GenerateNewBuildID", func(t *testing.T) { - mocks := setupBuildIDMocks(t, "", os.ErrNotExist, nil) - - // Given a BuildIDPipeline with no existing build ID - pipeline := NewBuildIDPipeline() - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When executing without new flag - err := pipeline.Execute(context.Background()) - - // Then it should succeed and generate a new build ID (may fail on read-only filesystem or permission denied) - if err != nil && !strings.Contains(err.Error(), "read-only file system") && !strings.Contains(err.Error(), "permission denied") { - t.Fatalf("Expected execution to succeed or fail with filesystem error, got error: %v", err) - } - }) - - t.Run("ForceNewBuildID", func(t *testing.T) { - mocks := setupBuildIDMocks(t, "250802.123.5", nil, nil) - - // Given a BuildIDPipeline with existing build ID - pipeline := NewBuildIDPipeline() - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When executing with new flag - ctx := context.WithValue(context.Background(), "new", true) - err := pipeline.Execute(ctx) - - // Then it should succeed and generate a new build ID (may fail on read-only filesystem or permission denied) - if err != nil && !strings.Contains(err.Error(), "read-only file system") && !strings.Contains(err.Error(), "permission denied") { - t.Fatalf("Expected execution to succeed or fail with filesystem error, got error: %v", err) - } - }) -} - -func TestBuildIDPipeline_getBuildID(t *testing.T) { - t.Run("ExistingBuildID", func(t *testing.T) { - mocks := setupBuildIDMocks(t, "250802.123.5", nil, nil) - - // Given a BuildIDPipeline with existing build ID - pipeline := NewBuildIDPipeline() - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When getting build ID - err := pipeline.getBuildID() - - // Then it should succeed and output the existing build ID - if err != nil { - t.Fatalf("Expected getBuildID to succeed, got error: %v", err) - } - }) - - t.Run("NoExistingBuildID", func(t *testing.T) { - mocks := setupBuildIDMocks(t, "", os.ErrNotExist, nil) - - // Given a BuildIDPipeline with no existing build ID - pipeline := NewBuildIDPipeline() - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When getting build ID - err := pipeline.getBuildID() - - // Then it should succeed and generate a new build ID (may fail on read-only filesystem) - if err != nil && !strings.Contains(err.Error(), "read-only file system") { - t.Fatalf("Expected getBuildID to succeed or fail with read-only filesystem, got error: %v", err) - } - }) - - t.Run("GetBuildIDFromFileError", func(t *testing.T) { - mocks := setupBuildIDMocks(t, "", nil, fmt.Errorf("mock read error")) - - // Given a BuildIDPipeline with read error - pipeline := NewBuildIDPipeline() - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When getting build ID - err := pipeline.getBuildID() - - // Then it should return an error - if err == nil { - t.Fatal("Expected getBuildID to fail with read error") - } - if !strings.Contains(err.Error(), "failed to read build ID file") { - t.Errorf("Expected error to contain 'failed to read build ID file', got: %v", err) - } - }) - - t.Run("GenerateBuildIDError", func(t *testing.T) { - mocks := setupBuildIDMocks(t, "", os.ErrNotExist, nil) - - // Given a BuildIDPipeline with no existing build ID - pipeline := NewBuildIDPipeline() - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // And mock shell returns error for project root - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("mock project root error") - } - - // When getting build ID - err := pipeline.getBuildID() - - // Then it should return an error - if err == nil { - t.Fatal("Expected getBuildID to fail with project root error") - } - if !strings.Contains(err.Error(), "failed to get project root") { - t.Errorf("Expected error to contain 'failed to get project root', got: %v", err) - } - }) -} - -func TestBuildIDPipeline_generateNewBuildID(t *testing.T) { - t.Run("Success", func(t *testing.T) { - mocks := setupBuildIDMocks(t, "", os.ErrNotExist, nil) - - // Given a BuildIDPipeline with no existing build ID - pipeline := NewBuildIDPipeline() - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When generating new build ID - err := pipeline.generateNewBuildID() - - // Then it should succeed and generate a new build ID (may fail on read-only filesystem) - if err != nil && !strings.Contains(err.Error(), "read-only file system") { - t.Fatalf("Expected generateNewBuildID to succeed or fail with read-only filesystem, got error: %v", err) - } - }) - - t.Run("ExistingBuildID", func(t *testing.T) { - mocks := setupBuildIDMocks(t, "250802.123.5", nil, nil) - - // Given a BuildIDPipeline with existing build ID - pipeline := NewBuildIDPipeline() - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When generating new build ID - err := pipeline.generateNewBuildID() - - // Then it should succeed and overwrite the existing build ID (may fail on read-only filesystem) - if err != nil && !strings.Contains(err.Error(), "read-only file system") { - t.Fatalf("Expected generateNewBuildID to succeed or fail with read-only filesystem, got error: %v", err) - } - }) -} - -func TestBuildIDPipeline_getBuildIDFromFile(t *testing.T) { - t.Run("ExistingBuildID", func(t *testing.T) { - mocks := setupBuildIDMocks(t, "250802.123.5", nil, nil) - - // Given a BuildIDPipeline with existing build ID - pipeline := NewBuildIDPipeline() - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When getting build ID from file - err := pipeline.getBuildID() - - // Then it should succeed and return the existing build ID - if err != nil { - t.Fatalf("Expected getBuildID to succeed, got error: %v", err) - } - // Note: getBuildID prints the build ID, so we can't easily test the return value - // The test verifies it doesn't error - }) - - t.Run("NoExistingBuildID", func(t *testing.T) { - mocks := setupBuildIDMocks(t, "", os.ErrNotExist, nil) - - // Given a BuildIDPipeline with no existing build ID - pipeline := NewBuildIDPipeline() - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When getting build ID from file - err := pipeline.getBuildID() - - // Then it should succeed and generate a new build ID (may fail on read-only filesystem) - if err != nil && !strings.Contains(err.Error(), "read-only file system") { - t.Fatalf("Expected getBuildID to succeed or fail with read-only filesystem, got error: %v", err) - } - }) - - t.Run("ReadError", func(t *testing.T) { - mocks := setupBuildIDMocks(t, "", nil, fmt.Errorf("mock read error")) - - // Given a BuildIDPipeline with read error - pipeline := NewBuildIDPipeline() - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When getting build ID from file - err := pipeline.getBuildID() - - // Then it should return an error - if err == nil { - t.Fatal("Expected getBuildID to fail with read error") - } - if !strings.Contains(err.Error(), "failed to read build ID file") { - t.Errorf("Expected error to contain 'failed to read build ID file', got: %v", err) - } - }) -} - -func TestBuildIDPipeline_setBuildIDToFile(t *testing.T) { - t.Run("Success", func(t *testing.T) { - mocks := setupBuildIDMocks(t, "", nil, nil) - - // Given a BuildIDPipeline - pipeline := NewBuildIDPipeline() - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When setting build ID to file - err := pipeline.writeBuildIDToFile("250802.123.5") - - // Then it should succeed (may fail on read-only filesystem, which is expected) - if err != nil && !strings.Contains(err.Error(), "read-only file system") { - t.Fatalf("Expected writeBuildIDToFile to succeed or fail with read-only filesystem, got error: %v", err) - } - }) - - t.Run("GetProjectRootError", func(t *testing.T) { - mocks := setupBuildIDMocks(t, "", nil, nil) - - // Given a BuildIDPipeline - pipeline := NewBuildIDPipeline() - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // And mock shell returns error for project root - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("mock project root error") - } - - // When setting build ID to file - err := pipeline.writeBuildIDToFile("250802.123.5") - - // Then it should return an error - if err == nil { - t.Fatal("Expected writeBuildIDToFile to fail with project root error") - } - if !strings.Contains(err.Error(), "failed to get project root") { - t.Errorf("Expected error to contain 'failed to get project root', got: %v", err) - } - }) -} - -func TestBuildIDPipeline_generateBuildID(t *testing.T) { - t.Run("Success", func(t *testing.T) { - mocks := setupBuildIDMocks(t, "", nil, nil) - - // Given a BuildIDPipeline - pipeline := NewBuildIDPipeline() - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When generating build ID - buildID, err := pipeline.generateBuildID() - - // Then it should succeed and return a build ID - if err != nil { - t.Fatalf("Expected generateBuildID to succeed, got error: %v", err) - } - if buildID == "" { - t.Fatal("Expected non-empty build ID") - } - - // And it should be in the correct YYMMDD.RANDOM.# format - parts := strings.Split(buildID, ".") - if len(parts) != 3 { - t.Errorf("Expected build ID to have 3 parts separated by dots, got %d parts: %s", len(parts), buildID) - } - - // Check date part (YYMMDD) - if len(parts[0]) != 6 { - t.Errorf("Expected date part to be 6 characters (YYMMDD), got %d: %s", len(parts[0]), parts[0]) - } - - // Check random part (3 digits) - if len(parts[1]) != 3 { - t.Errorf("Expected random part to be 3 digits, got %d: %s", len(parts[1]), parts[1]) - } - - // Check counter part (should be 1 for first build) - if parts[2] != "1" { - t.Errorf("Expected counter part to be 1 for first build, got %s", parts[2]) - } - }) - - t.Run("IncrementExistingBuildID", func(t *testing.T) { - // Get today's date for the test - now := time.Now() - today := fmt.Sprintf("%02d%02d%02d", now.Year()%100, int(now.Month()), now.Day()) - - // Mock existing build ID for today - existingBuildID := fmt.Sprintf("%s.123.5", today) - mocks := setupBuildIDMocks(t, existingBuildID, nil, nil) - - // Given a BuildIDPipeline with existing build ID - pipeline := NewBuildIDPipeline() - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When generating build ID - buildID, err := pipeline.generateBuildID() - - // Then it should succeed and increment the counter - if err != nil { - t.Fatalf("Expected generateBuildID to succeed, got error: %v", err) - } - - // Should increment counter from 5 to 6 - expectedBuildID := fmt.Sprintf("%s.123.6", today) - if buildID != expectedBuildID { - t.Errorf("Expected build ID to be %s, got %s", expectedBuildID, buildID) - } - }) - - t.Run("NewDayNewRandom", func(t *testing.T) { - // Get today's and yesterday's dates for the test - now := time.Now() - today := fmt.Sprintf("%02d%02d%02d", now.Year()%100, int(now.Month()), now.Day()) - yesterday := fmt.Sprintf("%02d%02d%02d", now.Year()%100, int(now.Month()), now.Day()-1) - - // Mock existing build ID from yesterday - existingBuildID := fmt.Sprintf("%s.456.10", yesterday) - mocks := setupBuildIDMocks(t, existingBuildID, nil, nil) - - // Given a BuildIDPipeline with existing build ID from different day - pipeline := NewBuildIDPipeline() - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When generating build ID - buildID, err := pipeline.generateBuildID() - - // Then it should succeed and generate new random with counter 1 - if err != nil { - t.Fatalf("Expected generateBuildID to succeed, got error: %v", err) - } - - // Should have today's date, new random, and counter 1 - parts := strings.Split(buildID, ".") - if len(parts) != 3 { - t.Errorf("Expected build ID to have 3 parts, got %d: %s", len(parts), buildID) - } - - // Date should be today - if parts[0] != today { - t.Errorf("Expected date to be today (%s), got %s", today, parts[0]) - } - - // Random should be different from yesterday (456) - if parts[1] == "456" { - t.Errorf("Expected new random number, got same as yesterday: %s", parts[1]) - } - - // Counter should be 1 for new day - if parts[2] != "1" { - t.Errorf("Expected counter to be 1 for new day, got %s", parts[2]) - } - }) -} - -func TestBuildIDPipeline_incrementBuildID(t *testing.T) { - t.Run("IncrementSameDay", func(t *testing.T) { - // Given a BuildIDPipeline - pipeline := NewBuildIDPipeline() - if err := pipeline.Initialize(setupBuildIDMocks(t, "", nil, nil).Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Get today's date for the test - now := time.Now() - today := fmt.Sprintf("%02d%02d%02d", now.Year()%100, int(now.Month()), now.Day()) - - // When incrementing existing build ID from same day - existingBuildID := fmt.Sprintf("%s.123.5", today) - newBuildID, err := pipeline.incrementBuildID(existingBuildID, today) - - // Then it should increment counter - if err != nil { - t.Fatalf("Expected incrementBuildID to succeed, got error: %v", err) - } - - expectedBuildID := fmt.Sprintf("%s.123.6", today) - if newBuildID != expectedBuildID { - t.Errorf("Expected build ID to be %s, got %s", expectedBuildID, newBuildID) - } - }) - - t.Run("NewDayNewRandom", func(t *testing.T) { - // Given a BuildIDPipeline - pipeline := NewBuildIDPipeline() - if err := pipeline.Initialize(setupBuildIDMocks(t, "", nil, nil).Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Get today's and yesterday's dates for the test - now := time.Now() - today := fmt.Sprintf("%02d%02d%02d", now.Year()%100, int(now.Month()), now.Day()) - yesterday := fmt.Sprintf("%02d%02d%02d", now.Year()%100, int(now.Month()), now.Day()-1) - - // When incrementing existing build ID from different day - existingBuildID := fmt.Sprintf("%s.456.10", yesterday) - newBuildID, err := pipeline.incrementBuildID(existingBuildID, today) - - // Then it should generate new random and reset counter - if err != nil { - t.Fatalf("Expected incrementBuildID to succeed, got error: %v", err) - } - - parts := strings.Split(newBuildID, ".") - if len(parts) != 3 { - t.Errorf("Expected build ID to have 3 parts, got %d: %s", len(parts), newBuildID) - } - - // Date should be current date - if parts[0] != today { - t.Errorf("Expected date to be %s, got %s", today, parts[0]) - } - - // Random should be different - if parts[1] == "456" { - t.Errorf("Expected new random number, got same: %s", parts[1]) - } - - // Counter should be 1 - if parts[2] != "1" { - t.Errorf("Expected counter to be 1, got %s", parts[2]) - } - }) - - t.Run("InvalidFormat", func(t *testing.T) { - // Given a BuildIDPipeline - pipeline := NewBuildIDPipeline() - if err := pipeline.Initialize(setupBuildIDMocks(t, "", nil, nil).Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When incrementing invalid build ID format - invalidBuildID := "invalid-format" - currentDate := "250802" - _, err := pipeline.incrementBuildID(invalidBuildID, currentDate) - - // Then it should return an error - if err == nil { - t.Fatal("Expected incrementBuildID to fail with invalid format") - } - if !strings.Contains(err.Error(), "invalid build ID format") { - t.Errorf("Expected error to contain 'invalid build ID format', got: %v", err) - } - }) - - t.Run("InvalidCounter", func(t *testing.T) { - // Given a BuildIDPipeline - pipeline := NewBuildIDPipeline() - if err := pipeline.Initialize(setupBuildIDMocks(t, "", nil, nil).Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When incrementing build ID with invalid counter - invalidBuildID := "250802.123.invalid" - currentDate := "250802" - _, err := pipeline.incrementBuildID(invalidBuildID, currentDate) - - // Then it should return an error - if err == nil { - t.Fatal("Expected incrementBuildID to fail with invalid counter") - } - if !strings.Contains(err.Error(), "invalid counter component") { - t.Errorf("Expected error to contain 'invalid counter component', got: %v", err) - } - }) -} diff --git a/pkg/pipelines/check.go b/pkg/pipelines/check.go deleted file mode 100644 index 9e4a00b40..000000000 --- a/pkg/pipelines/check.go +++ /dev/null @@ -1,315 +0,0 @@ -package pipelines - -import ( - "context" - "fmt" - "time" - - "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/context/tools" - "github.com/windsorcli/cli/pkg/provisioner/cluster" - "github.com/windsorcli/cli/pkg/provisioner/kubernetes" -) - -// The CheckPipeline is a specialized component that manages tool version checking and node health checking functionality. -// It provides check-specific command execution including tools verification and cluster node health validation, -// configuration validation, and shell integration for the Windsor CLI check command. -// The CheckPipeline handles both basic tool checking and advanced node health monitoring operations. - -// ============================================================================= -// Types -// ============================================================================= - -// CheckPipeline implements health checking functionality for tools and cluster nodes -type CheckPipeline struct { - BasePipeline - - toolsManager tools.ToolsManager - clusterClient cluster.ClusterClient - kubernetesManager kubernetes.KubernetesManager -} - -// ============================================================================= -// Constructor -// ============================================================================= - -// NewCheckPipeline creates a new CheckPipeline instance -func NewCheckPipeline() *CheckPipeline { - return &CheckPipeline{ - BasePipeline: *NewBasePipeline(), - } -} - -// ============================================================================= -// Public Methods -// ============================================================================= - -// Initialize sets up the CheckPipeline by resolving dependencies -func (p *CheckPipeline) Initialize(injector di.Injector, ctx context.Context) error { - if err := p.BasePipeline.Initialize(injector, ctx); err != nil { - return err - } - - p.toolsManager = p.withToolsManager() - p.clusterClient = p.withClusterClient() - p.withKubernetesClient() - p.kubernetesManager = p.withKubernetesManager() - - if p.toolsManager != nil { - if err := p.toolsManager.Initialize(); err != nil { - return fmt.Errorf("failed to initialize tools manager: %w", err) - } - } - - if p.kubernetesManager != nil { - if err := p.kubernetesManager.Initialize(); err != nil { - return fmt.Errorf("failed to initialize kubernetes manager: %w", err) - } - } - - return nil -} - -// Execute performs the check operation based on the operation type specified in the context. -// It supports both "tools" and "node-health" operations, validating configuration and -// executing the appropriate check functionality with proper error handling and output formatting. -func (p *CheckPipeline) Execute(ctx context.Context) error { - if !p.configHandler.IsLoaded() { - return fmt.Errorf("Nothing to check. Have you run \033[1mwindsor init\033[0m?") - } - - operation := ctx.Value("operation") - if operation == nil { - return p.executeToolsCheck(ctx) - } - - operationType, ok := operation.(string) - if !ok { - return fmt.Errorf("Invalid operation type") - } - - switch operationType { - case "tools": - return p.executeToolsCheck(ctx) - case "node-health": - return p.executeNodeHealthCheck(ctx) - default: - return fmt.Errorf("Unknown operation type: %s", operationType) - } -} - -// ============================================================================= -// Private Methods -// ============================================================================= - -// executeToolsCheck performs tool version checking using the tools manager. -// It validates that all required tools are installed and meet minimum version requirements, -// providing success output when all tools are up to date. -func (p *CheckPipeline) executeToolsCheck(ctx context.Context) error { - if err := p.toolsManager.Check(); err != nil { - return fmt.Errorf("Error checking tools: %w", err) - } - - outputFunc := ctx.Value("output") - if outputFunc != nil { - if fn, ok := outputFunc.(func(string)); ok { - fn("All tools are up to date.") - } - } - - return nil -} - -// executeNodeHealthCheck performs cluster node health checking using the cluster client. -// It validates node health status and optionally checks for specific versions. -// Nodes must be specified via context parameters. Supports timeout configuration. -// If the Kubernetes endpoint flag is provided, performs Kubernetes API health check. -// Outputs status via the output function in context if present. -func (p *CheckPipeline) executeNodeHealthCheck(ctx context.Context) error { - nodes := ctx.Value("nodes") - k8sEndpointProvided := ctx.Value("k8s-endpoint-provided") - - var hasNodeCheck bool - var hasK8sCheck bool - - if nodes != nil { - if nodeAddresses, ok := nodes.([]string); ok && len(nodeAddresses) > 0 { - hasNodeCheck = true - } - } - - if k8sEndpointProvided != nil { - if provided, ok := k8sEndpointProvided.(bool); ok && provided { - hasK8sCheck = true - } - } - - if !hasNodeCheck && !hasK8sCheck { - return fmt.Errorf("No health checks specified. Use --nodes and/or --k8s-endpoint flags to specify health checks to perform") - } - - // If we have nodes but no cluster client and no k8s endpoint, we can't perform any checks - if hasNodeCheck && p.clusterClient == nil && !hasK8sCheck { - return fmt.Errorf("No health checks specified. Use --nodes and/or --k8s-endpoint flags to specify health checks to perform") - } - - // Handle provider-specific node health checks (when --nodes is provided and cluster client is available) - if hasNodeCheck && p.clusterClient != nil { - defer p.clusterClient.Close() - - nodeAddresses, ok := nodes.([]string) - if !ok { - return fmt.Errorf("Invalid nodes parameter type") - } - - timeout := ctx.Value("timeout") - var timeoutDuration time.Duration - if timeout != nil { - if t, ok := timeout.(time.Duration); ok { - timeoutDuration = t - } - } - - version := ctx.Value("version") - var expectedVersion string - if version != nil { - if v, ok := version.(string); ok { - expectedVersion = v - } - } - - // Perform provider-specific node health checks - var checkCtx context.Context - var cancel context.CancelFunc - if timeoutDuration > 0 { - checkCtx, cancel = context.WithTimeout(ctx, timeoutDuration) - } else { - checkCtx, cancel = context.WithCancel(ctx) - } - defer cancel() - - if err := p.clusterClient.WaitForNodesHealthy(checkCtx, nodeAddresses, expectedVersion); err != nil { - // If cluster client fails and we have k8s endpoint, continue with k8s checks - if hasK8sCheck { - fmt.Printf("Warning: Cluster client failed (%v), continuing with Kubernetes checks\n", err) - } else { - return fmt.Errorf("nodes failed health check: %w", err) - } - } else { - outputFunc := ctx.Value("output") - if outputFunc != nil { - if fn, ok := outputFunc.(func(string)); ok { - message := fmt.Sprintf("All %d nodes are healthy", len(nodeAddresses)) - if expectedVersion != "" { - message += fmt.Sprintf(" and running version %s", expectedVersion) - } - fn(message) - } - } - } - } - - // Handle Kubernetes health checks (API + optional node Ready state) - if hasK8sCheck { - if p.kubernetesManager == nil { - return fmt.Errorf("No kubernetes manager found") - } - - k8sEndpoint := ctx.Value("k8s-endpoint") - var k8sEndpointStr string - if k8sEndpoint != nil { - if e, ok := k8sEndpoint.(string); ok { - if e == "true" { - k8sEndpointStr = "" - } else { - k8sEndpointStr = e - } - } - } - - // Only include nodes in the K8s health check if --ready flag is explicitly specified - var nodeNames []string - checkNodeReady := ctx.Value("check-node-ready") - if checkNodeReady != nil { - if ready, ok := checkNodeReady.(bool); ok && ready { - if hasNodeCheck { - // If specific nodes are provided, check those nodes - if nodeAddresses, ok := nodes.([]string); ok { - nodeNames = nodeAddresses - } - } else { - // If --ready is specified but no --nodes are provided, return an error - return fmt.Errorf("--ready flag requires --nodes to be specified") - } - } - } - - // Show waiting message if we're going to check node readiness - if len(nodeNames) > 0 { - outputFunc := ctx.Value("output") - if outputFunc != nil { - if fn, ok := outputFunc.(func(string)); ok { - fn(fmt.Sprintf("Waiting for %d nodes to be Ready...", len(nodeNames))) - } - } - } - - // Get output function for progress feedback - var progressOutputFunc func(string) - output := ctx.Value("output") - if output != nil { - if fn, ok := output.(func(string)); ok { - progressOutputFunc = fn - } - } - - if err := p.kubernetesManager.WaitForKubernetesHealthy(ctx, k8sEndpointStr, progressOutputFunc, nodeNames...); err != nil { - return fmt.Errorf("Kubernetes health check failed: %w", err) - } - - outputFunc := ctx.Value("output") - if outputFunc != nil { - if fn, ok := outputFunc.(func(string)); ok { - if len(nodeNames) > 0 { - // Check if all requested nodes were found and ready - readyStatus, err := p.kubernetesManager.GetNodeReadyStatus(ctx, nodeNames) - allFoundAndReady := err == nil && len(readyStatus) == len(nodeNames) - for _, ready := range readyStatus { - if !ready { - allFoundAndReady = false - break - } - } - - if allFoundAndReady { - if k8sEndpointStr != "" { - fn(fmt.Sprintf("Kubernetes API endpoint %s is healthy and all nodes are Ready", k8sEndpointStr)) - } else { - fn("Kubernetes API endpoint (kubeconfig default) is healthy and all nodes are Ready") - } - } else { - if k8sEndpointStr != "" { - fn(fmt.Sprintf("Kubernetes API endpoint %s is healthy", k8sEndpointStr)) - } else { - fn("Kubernetes API endpoint (kubeconfig default) is healthy") - } - } - } else { - if k8sEndpointStr != "" { - fn(fmt.Sprintf("Kubernetes API endpoint %s is healthy", k8sEndpointStr)) - } else { - fn("Kubernetes API endpoint (kubeconfig default) is healthy") - } - } - } - } - } - - return nil -} - -// ============================================================================= -// Interface Compliance -// ============================================================================= - -var _ Pipeline = (*CheckPipeline)(nil) diff --git a/pkg/pipelines/check_test.go b/pkg/pipelines/check_test.go deleted file mode 100644 index e8524177a..000000000 --- a/pkg/pipelines/check_test.go +++ /dev/null @@ -1,1182 +0,0 @@ -package pipelines - -import ( - "context" - "fmt" - "os" - "strings" - "testing" - "time" - - "github.com/windsorcli/cli/pkg/context/config" - "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/context/tools" - "github.com/windsorcli/cli/pkg/provisioner/cluster" - "github.com/windsorcli/cli/pkg/provisioner/kubernetes" - "github.com/windsorcli/cli/pkg/context/shell" -) - -// mockFileInfo implements os.FileInfo for testing -type mockFileInfo struct{} - -func (m *mockFileInfo) Name() string { return "windsor.yaml" } -func (m *mockFileInfo) Size() int64 { return 100 } -func (m *mockFileInfo) Mode() os.FileMode { return 0644 } -func (m *mockFileInfo) ModTime() time.Time { return time.Now() } -func (m *mockFileInfo) IsDir() bool { return false } -func (m *mockFileInfo) Sys() interface{} { return nil } - -// ============================================================================= -// Test Setup Infrastructure -// ============================================================================= - -// CheckMocks extends the base Mocks with check-specific dependencies -type CheckMocks struct { - *Mocks - ToolsManager *tools.MockToolsManager - ClusterClient *cluster.MockClusterClient - KubernetesManager *kubernetes.MockKubernetesManager -} - -// setupCheckMocks creates mocks for check pipeline tests -func setupCheckMocks(t *testing.T, opts ...*SetupOptions) *CheckMocks { - t.Helper() - - // Create setup options, preserving any provided options - setupOptions := &SetupOptions{} - if len(opts) > 0 && opts[0] != nil { - setupOptions = opts[0] - } - - // Only create a default mock config handler if one wasn't provided - if setupOptions.ConfigHandler == nil { - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.InitializeFunc = func() error { return nil } - mockConfigHandler.IsLoadedFunc = func() bool { return true } // Default to loaded - setupOptions.ConfigHandler = mockConfigHandler - } - - baseMocks := setupMocks(t, setupOptions) - - // Create check-specific mocks only if they don't already exist - var mockToolsManager *tools.MockToolsManager - if existing := baseMocks.Injector.Resolve("toolsManager"); existing != nil { - if tm, ok := existing.(*tools.MockToolsManager); ok { - mockToolsManager = tm - } else { - // If existing is not a MockToolsManager, create a new one - mockToolsManager = tools.NewMockToolsManager() - mockToolsManager.InitializeFunc = func() error { return nil } - mockToolsManager.CheckFunc = func() error { return nil } - baseMocks.Injector.Register("toolsManager", mockToolsManager) - } - } else { - mockToolsManager = tools.NewMockToolsManager() - mockToolsManager.InitializeFunc = func() error { return nil } - mockToolsManager.CheckFunc = func() error { return nil } - baseMocks.Injector.Register("toolsManager", mockToolsManager) - } - - var mockClusterClient *cluster.MockClusterClient - if existing := baseMocks.Injector.Resolve("clusterClient"); existing != nil { - if cc, ok := existing.(*cluster.MockClusterClient); ok { - mockClusterClient = cc - } else { - // If existing is not a MockClusterClient, create a new one - mockClusterClient = cluster.NewMockClusterClient() - mockClusterClient.WaitForNodesHealthyFunc = func(ctx context.Context, nodeAddresses []string, expectedVersion string) error { - return nil - } - baseMocks.Injector.Register("clusterClient", mockClusterClient) - } - } else { - mockClusterClient = cluster.NewMockClusterClient() - mockClusterClient.WaitForNodesHealthyFunc = func(ctx context.Context, nodeAddresses []string, expectedVersion string) error { - return nil - } - baseMocks.Injector.Register("clusterClient", mockClusterClient) - } - - // Create kubernetes manager mock - var mockKubernetesManager *kubernetes.MockKubernetesManager - if existing := baseMocks.Injector.Resolve("kubernetesManager"); existing != nil { - if km, ok := existing.(*kubernetes.MockKubernetesManager); ok { - mockKubernetesManager = km - } else { - // If existing is not a MockKubernetesManager, create a new one - mockKubernetesManager = kubernetes.NewMockKubernetesManager(baseMocks.Injector) - mockKubernetesManager.InitializeFunc = func() error { return nil } - mockKubernetesManager.WaitForKubernetesHealthyFunc = func(ctx context.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { - return nil - } - baseMocks.Injector.Register("kubernetesManager", mockKubernetesManager) - } - } else { - mockKubernetesManager = kubernetes.NewMockKubernetesManager(baseMocks.Injector) - mockKubernetesManager.InitializeFunc = func() error { return nil } - mockKubernetesManager.WaitForKubernetesHealthyFunc = func(ctx context.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { - return nil - } - baseMocks.Injector.Register("kubernetesManager", mockKubernetesManager) - } - - return &CheckMocks{ - Mocks: baseMocks, - ToolsManager: mockToolsManager, - ClusterClient: mockClusterClient, - KubernetesManager: mockKubernetesManager, - } -} - -// ============================================================================= -// Test Constructor -// ============================================================================= - -func TestNewCheckPipeline(t *testing.T) { - t.Run("CreatesWithDefaults", func(t *testing.T) { - // Given creating a new check pipeline - pipeline := NewCheckPipeline() - - // Then pipeline should not be nil - if pipeline == nil { - t.Fatal("Expected pipeline to not be nil") - } - }) -} - -// ============================================================================= -// Test Public Methods - Initialize -// ============================================================================= - -func TestCheckPipeline_Initialize(t *testing.T) { - setup := func(t *testing.T, opts ...*SetupOptions) (*CheckPipeline, *CheckMocks) { - t.Helper() - pipeline := NewCheckPipeline() - mocks := setupCheckMocks(t, opts...) - return pipeline, mocks - } - - t.Run("Success", func(t *testing.T) { - // Given a check pipeline - pipeline, mocks := setup(t) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("ReturnsErrorWhenConfigHandlerInitializeFails", func(t *testing.T) { - // Given a check pipeline with failing config handler initialization - pipeline := NewCheckPipeline() - - // Create injector and register failing config handler directly - injector := di.NewInjector() - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.InitializeFunc = func() error { - return fmt.Errorf("config initialization failed") - } - injector.Register("configHandler", mockConfigHandler) - - // Create and register basic shell - mockShell := shell.NewMockShell() - mockShell.InitializeFunc = func() error { return nil } - mockShell.GetProjectRootFunc = func() (string, error) { return t.TempDir(), nil } - injector.Register("shell", mockShell) - - // Register shims - shims := setupShims(t) - injector.Register("shims", shims) - - // When initializing the pipeline - err := pipeline.Initialize(injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "failed to initialize config handler: config initialization failed" { - t.Errorf("Expected config handler error, got: %v", err) - } - }) - - t.Run("ReturnsErrorWhenShellInitializeFails", func(t *testing.T) { - // Given a check pipeline with failing shell initialization - pipeline, mocks := setup(t) - - mocks.Shell.InitializeFunc = func() error { - return fmt.Errorf("shell initialization failed") - } - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "failed to initialize shell: shell initialization failed" { - t.Errorf("Expected shell error, got: %v", err) - } - }) - - t.Run("ReturnsErrorWhenToolsManagerInitializeFails", func(t *testing.T) { - // Given a check pipeline with failing tools manager initialization - pipeline := NewCheckPipeline() - mocks := setupCheckMocks(t) - - mocks.ToolsManager.InitializeFunc = func() error { - return fmt.Errorf("tools manager initialization failed") - } - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "failed to initialize tools manager: tools manager initialization failed" { - t.Errorf("Expected tools manager error, got: %v", err) - } - }) - - t.Run("ReturnsErrorWhenLoadConfigFails", func(t *testing.T) { - // Given a check pipeline with failing config loading - pipeline := NewCheckPipeline() - - // Create injector and register failing config handler directly - injector := di.NewInjector() - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.InitializeFunc = func() error { return nil } - mockConfigHandler.LoadConfigFunc = func() error { - return fmt.Errorf("config loading failed") - } - injector.Register("configHandler", mockConfigHandler) - - // Create and register basic shell - mockShell := shell.NewMockShell() - mockShell.InitializeFunc = func() error { return nil } - mockShell.GetProjectRootFunc = func() (string, error) { return t.TempDir(), nil } - injector.Register("shell", mockShell) - - // Register shims that simulate config file exists - shims := setupShims(t) - shims.Stat = func(name string) (os.FileInfo, error) { - // Simulate windsor.yaml exists - if strings.HasSuffix(name, "windsor.yaml") { - return &mockFileInfo{}, nil - } - return nil, os.ErrNotExist - } - injector.Register("shims", shims) - - // When initializing the pipeline - err := pipeline.Initialize(injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "failed to load context config: config loading failed" { - t.Errorf("Expected config loading error, got: %v", err) - } - }) - - t.Run("ReusesExistingComponentsFromDIContainer", func(t *testing.T) { - // Given a check pipeline with pre-registered components - injector := di.NewInjector() - existingToolsManager := tools.NewMockToolsManager() - existingToolsManager.InitializeFunc = func() error { return nil } - injector.Register("toolsManager", existingToolsManager) - - pipeline, mocks := setup(t, &SetupOptions{ - Injector: injector, - }) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then no error should be returned and existing components should be reused - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - resolvedToolsManager := mocks.Injector.Resolve("toolsManager") - if resolvedToolsManager != existingToolsManager { - t.Error("Expected existing tools manager to be reused") - } - }) -} - -// ============================================================================= -// Test Public Methods - Execute -// ============================================================================= - -func TestCheckPipeline_Execute(t *testing.T) { - setup := func(t *testing.T, opts ...*SetupOptions) (*CheckPipeline, *CheckMocks) { - t.Helper() - pipeline := NewCheckPipeline() - mocks := setupCheckMocks(t, opts...) - - // Initialize the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - return pipeline, mocks - } - - t.Run("ExecutesToolsCheckByDefault", func(t *testing.T) { - // Given a check pipeline - pipeline, mocks := setup(t) - - checkCalled := false - mocks.ToolsManager.CheckFunc = func() error { - checkCalled = true - return nil - } - - // When executing the pipeline - err := pipeline.Execute(context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And tools check should be called - if !checkCalled { - t.Error("Expected tools check to be called") - } - }) - - t.Run("ExecutesToolsCheckExplicitly", func(t *testing.T) { - // Given a check pipeline with explicit tools operation - pipeline, mocks := setup(t) - - checkCalled := false - mocks.ToolsManager.CheckFunc = func() error { - checkCalled = true - return nil - } - - ctx := context.WithValue(context.Background(), "operation", "tools") - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And tools check should be called - if !checkCalled { - t.Error("Expected tools check to be called") - } - }) - - t.Run("ExecutesNodeHealthCheck", func(t *testing.T) { - // Given a check pipeline with node health operation - pipeline, mocks := setup(t) - - waitCalled := false - mocks.ClusterClient.WaitForNodesHealthyFunc = func(ctx context.Context, nodeAddresses []string, expectedVersion string) error { - waitCalled = true - return nil - } - - ctx := context.WithValue(context.Background(), "operation", "node-health") - ctx = context.WithValue(ctx, "nodes", []string{"node1", "node2"}) - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And node health check should be called - if !waitCalled { - t.Error("Expected node health check to be called") - } - }) - - t.Run("ExecutesNodeHealthCheckWithVersion", func(t *testing.T) { - // Given a check pipeline with node health operation and version - pipeline, mocks := setup(t) - - var capturedVersion string - mocks.ClusterClient.WaitForNodesHealthyFunc = func(ctx context.Context, nodeAddresses []string, expectedVersion string) error { - capturedVersion = expectedVersion - return nil - } - - ctx := context.WithValue(context.Background(), "operation", "node-health") - ctx = context.WithValue(ctx, "nodes", []string{"node1"}) - ctx = context.WithValue(ctx, "version", "v1.30.0") - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And node health check should be called with version - if capturedVersion != "v1.30.0" { - t.Errorf("Expected version v1.30.0, got %s", capturedVersion) - } - }) - - t.Run("ReturnsErrorWhenConfigNotLoaded", func(t *testing.T) { - // Given a check pipeline with config not loaded - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.InitializeFunc = func() error { return nil } - mockConfigHandler.IsLoadedFunc = func() bool { return false } - - pipeline, _ := setup(t, &SetupOptions{ - ConfigHandler: mockConfigHandler, - }) - - // When executing the pipeline - err := pipeline.Execute(context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - expectedMsg := "Nothing to check. Have you run \033[1mwindsor init\033[0m?" - if err.Error() != expectedMsg { - t.Errorf("Expected config not loaded error, got: %v", err) - } - }) - - t.Run("ReturnsErrorForInvalidOperationType", func(t *testing.T) { - // Given a check pipeline with invalid operation type - pipeline, _ := setup(t) - - ctx := context.WithValue(context.Background(), "operation", 123) - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "Invalid operation type" { - t.Errorf("Expected operation type error, got: %v", err) - } - }) - - t.Run("ReturnsErrorForUnknownOperation", func(t *testing.T) { - // Given a check pipeline with unknown operation - pipeline, _ := setup(t) - - ctx := context.WithValue(context.Background(), "operation", "unknown") - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "Unknown operation type: unknown" { - t.Errorf("Expected unknown operation error, got: %v", err) - } - }) - - t.Run("ReturnsErrorWhenToolsCheckFails", func(t *testing.T) { - // Given a check pipeline with failing tools check - pipeline, mocks := setup(t) - - mocks.ToolsManager.CheckFunc = func() error { - return fmt.Errorf("tools check failed") - } - - // When executing the pipeline - err := pipeline.Execute(context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "Error checking tools: tools check failed" { - t.Errorf("Expected tools check error, got: %v", err) - } - }) - - t.Run("ReturnsErrorWhenNodeHealthCheckFails", func(t *testing.T) { - // Given a check pipeline with failing node health check - pipeline, mocks := setup(t) - - mocks.ClusterClient.WaitForNodesHealthyFunc = func(ctx context.Context, nodeAddresses []string, expectedVersion string) error { - return fmt.Errorf("node health check failed") - } - - ctx := context.WithValue(context.Background(), "operation", "node-health") - ctx = context.WithValue(ctx, "nodes", []string{"node1"}) - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "nodes failed health check: node health check failed" { - t.Errorf("Expected node health check error, got: %v", err) - } - }) -} - -// ============================================================================= -// Test Private Methods - executeToolsCheck -// ============================================================================= - -func TestCheckPipeline_executeToolsCheck(t *testing.T) { - setup := func(t *testing.T) (*CheckPipeline, *CheckMocks) { - t.Helper() - pipeline := NewCheckPipeline() - mocks := setupCheckMocks(t) - - // Initialize the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - return pipeline, mocks - } - - t.Run("Success", func(t *testing.T) { - // Given a check pipeline - pipeline, mocks := setup(t) - - checkCalled := false - mocks.ToolsManager.CheckFunc = func() error { - checkCalled = true - return nil - } - - // When executing tools check - err := pipeline.executeToolsCheck(context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And tools check should be called - if !checkCalled { - t.Error("Expected tools check to be called") - } - }) - - t.Run("ReturnsErrorWhenToolsManagerCheckFails", func(t *testing.T) { - // Given a check pipeline with failing tools manager - pipeline, mocks := setup(t) - - mocks.ToolsManager.CheckFunc = func() error { - return fmt.Errorf("tools check failed") - } - - // When executing tools check - err := pipeline.executeToolsCheck(context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "Error checking tools: tools check failed" { - t.Errorf("Expected tools check error, got: %v", err) - } - }) - - t.Run("HandlesNoOutputFunction", func(t *testing.T) { - // Given a check pipeline with no output function - pipeline, _ := setup(t) - - // When executing tools check - err := pipeline.executeToolsCheck(context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) -} - -// ============================================================================= -// Test Private Methods - executeNodeHealthCheck -// ============================================================================= - -func TestCheckPipeline_executeNodeHealthCheck(t *testing.T) { - setup := func(t *testing.T) (*CheckPipeline, *CheckMocks) { - t.Helper() - pipeline := NewCheckPipeline() - mocks := setupCheckMocks(t) - - // Initialize the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - return pipeline, mocks - } - - t.Run("Success", func(t *testing.T) { - // Given a check pipeline - pipeline, mocks := setup(t) - - waitCalled := false - mocks.ClusterClient.WaitForNodesHealthyFunc = func(ctx context.Context, nodeAddresses []string, expectedVersion string) error { - waitCalled = true - return nil - } - - ctx := context.WithValue(context.Background(), "nodes", []string{"node1", "node2"}) - - // When executing node health check - err := pipeline.executeNodeHealthCheck(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And node health check should be called - if !waitCalled { - t.Error("Expected node health check to be called") - } - }) - - t.Run("SuccessWithVersion", func(t *testing.T) { - // Given a check pipeline with version specified - pipeline, mocks := setup(t) - - var capturedVersion string - mocks.ClusterClient.WaitForNodesHealthyFunc = func(ctx context.Context, nodeAddresses []string, expectedVersion string) error { - capturedVersion = expectedVersion - return nil - } - - ctx := context.WithValue(context.Background(), "nodes", []string{"node1"}) - ctx = context.WithValue(ctx, "version", "v1.30.0") - - // When executing node health check - err := pipeline.executeNodeHealthCheck(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And node health check should be called with version - if capturedVersion != "v1.30.0" { - t.Errorf("Expected version v1.30.0, got %s", capturedVersion) - } - }) - - t.Run("SuccessWithTimeout", func(t *testing.T) { - // Given a check pipeline with timeout specified - pipeline, _ := setup(t) - - ctx := context.WithValue(context.Background(), "nodes", []string{"node1"}) - ctx = context.WithValue(ctx, "timeout", 30*time.Second) - - // When executing node health check - err := pipeline.executeNodeHealthCheck(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("SucceedsWhenClusterClientIsNilButK8sEndpointProvided", func(t *testing.T) { - // Given a check pipeline with nil cluster client but k8s endpoint provided - pipeline, mocks := setup(t) - pipeline.clusterClient = nil - - mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx context.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { - return nil - } - - ctx := context.WithValue(context.Background(), "nodes", []string{"node1"}) - ctx = context.WithValue(ctx, "k8s-endpoint-provided", true) - ctx = context.WithValue(ctx, "k8s-endpoint", "") - ctx = context.WithValue(ctx, "output", func(msg string) {}) - - // When executing node health check - err := pipeline.executeNodeHealthCheck(ctx) - - // Then no error should be returned (k8s check succeeds) - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("ReturnsErrorWhenClusterClientIsNilAndNoK8sEndpoint", func(t *testing.T) { - // Given a check pipeline with nil cluster client and no k8s endpoint - pipeline, _ := setup(t) - pipeline.clusterClient = nil - - ctx := context.WithValue(context.Background(), "nodes", []string{"node1"}) - - // When executing node health check - err := pipeline.executeNodeHealthCheck(ctx) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "No health checks specified. Use --nodes and/or --k8s-endpoint flags to specify health checks to perform" { - t.Errorf("Expected health checks required error, got: %v", err) - } - }) - - t.Run("ReturnsErrorWhenNoHealthChecksSpecified", func(t *testing.T) { - // Given a check pipeline with no health checks specified - pipeline, _ := setup(t) - - // When executing node health check - err := pipeline.executeNodeHealthCheck(context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "No health checks specified. Use --nodes and/or --k8s-endpoint flags to specify health checks to perform" { - t.Errorf("Expected health checks required error, got: %v", err) - } - }) - - t.Run("ReturnsErrorWhenNodesParameterIsInvalidType", func(t *testing.T) { - // Given a check pipeline with invalid nodes parameter type - pipeline, _ := setup(t) - - ctx := context.WithValue(context.Background(), "nodes", "invalid") - - // When executing node health check - err := pipeline.executeNodeHealthCheck(ctx) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "No health checks specified. Use --nodes and/or --k8s-endpoint flags to specify health checks to perform" { - t.Errorf("Expected health checks required error, got: %v", err) - } - }) - - t.Run("SucceedsWhenOnlyK8sEndpointSpecified", func(t *testing.T) { - // Given a check pipeline with only k8s endpoint specified - pipeline, mocks := setup(t) - - mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx context.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { - return nil - } - - ctx := context.WithValue(context.Background(), "k8s-endpoint-provided", true) - ctx = context.WithValue(ctx, "k8s-endpoint", "") - ctx = context.WithValue(ctx, "output", func(msg string) {}) - - // When executing node health check - err := pipeline.executeNodeHealthCheck(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("ReturnsErrorWhenWaitForNodesHealthyFails", func(t *testing.T) { - // Given a check pipeline with failing cluster client - pipeline, mocks := setup(t) - - mocks.ClusterClient.WaitForNodesHealthyFunc = func(ctx context.Context, nodeAddresses []string, expectedVersion string) error { - return fmt.Errorf("node health check failed") - } - - ctx := context.WithValue(context.Background(), "nodes", []string{"node1"}) - - // When executing node health check - err := pipeline.executeNodeHealthCheck(ctx) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "nodes failed health check: node health check failed" { - t.Errorf("Expected node health check error, got: %v", err) - } - }) - - t.Run("HandlesNoOutputFunction", func(t *testing.T) { - // Given a check pipeline with no output function - pipeline, _ := setup(t) - - ctx := context.WithValue(context.Background(), "nodes", []string{"node1"}) - - // When executing node health check - err := pipeline.executeNodeHealthCheck(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - // ============================================================================= - // --ready flag tests - // ============================================================================= - - t.Run("K8sEndpointOnly_NoReadyFlag", func(t *testing.T) { - // Given a check pipeline with only k8s endpoint specified (no --ready) - pipeline, mocks := setup(t) - - var capturedNodeNames []string - mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx context.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { - capturedNodeNames = nodeNames - return nil - } - - ctx := context.WithValue(context.Background(), "k8s-endpoint-provided", true) - ctx = context.WithValue(ctx, "k8s-endpoint", "") - ctx = context.WithValue(ctx, "output", func(msg string) {}) - - // When executing node health check - err := pipeline.executeNodeHealthCheck(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And no node names should be passed (no readiness check) - if len(capturedNodeNames) != 0 { - t.Errorf("Expected no node names, got %v", capturedNodeNames) - } - }) - - t.Run("K8sEndpointWithReadyFlag_NoSpecificNodes", func(t *testing.T) { - // Given a check pipeline with k8s endpoint and --ready flag (no specific nodes) - pipeline, _ := setup(t) - - ctx := context.WithValue(context.Background(), "k8s-endpoint-provided", true) - ctx = context.WithValue(ctx, "k8s-endpoint", "") - ctx = context.WithValue(ctx, "check-node-ready", true) - ctx = context.WithValue(ctx, "output", func(msg string) {}) - - // When executing node health check - err := pipeline.executeNodeHealthCheck(ctx) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "--ready flag requires --nodes to be specified" { - t.Errorf("Expected error about --ready requiring --nodes, got: %v", err) - } - }) - - t.Run("K8sEndpointWithReadyFlag_SpecificNodes", func(t *testing.T) { - // Given a check pipeline with k8s endpoint, --ready flag, and specific nodes - pipeline, mocks := setup(t) - - var capturedNodeNames []string - mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx context.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { - capturedNodeNames = nodeNames - return nil - } - - ctx := context.WithValue(context.Background(), "k8s-endpoint-provided", true) - ctx = context.WithValue(ctx, "k8s-endpoint", "") - ctx = context.WithValue(ctx, "check-node-ready", true) - ctx = context.WithValue(ctx, "nodes", []string{"specific-node1", "specific-node2"}) - ctx = context.WithValue(ctx, "output", func(msg string) {}) - - // When executing node health check - err := pipeline.executeNodeHealthCheck(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And specific node names should be passed for readiness check - expectedNodeNames := []string{"specific-node1", "specific-node2"} - if len(capturedNodeNames) != len(expectedNodeNames) { - t.Errorf("Expected %d node names, got %d", len(expectedNodeNames), len(capturedNodeNames)) - } - for i, name := range expectedNodeNames { - if capturedNodeNames[i] != name { - t.Errorf("Expected node name %s at index %d, got %s", name, i, capturedNodeNames[i]) - } - } - }) - - t.Run("K8sEndpointWithNodes_NoReadyFlag", func(t *testing.T) { - // Given a check pipeline with k8s endpoint and specific nodes but no --ready flag - pipeline, mocks := setup(t) - - var capturedNodeNames []string - mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx context.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { - capturedNodeNames = nodeNames - return nil - } - - ctx := context.WithValue(context.Background(), "k8s-endpoint-provided", true) - ctx = context.WithValue(ctx, "k8s-endpoint", "") - ctx = context.WithValue(ctx, "nodes", []string{"node1", "node2"}) - ctx = context.WithValue(ctx, "output", func(msg string) {}) - - // When executing node health check - err := pipeline.executeNodeHealthCheck(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And no node names should be passed (no readiness check) - if len(capturedNodeNames) != 0 { - t.Errorf("Expected no node names, got %v", capturedNodeNames) - } - }) - - t.Run("ReadyFlagOnly_NoK8sEndpoint", func(t *testing.T) { - // Given a check pipeline with only --ready flag (no k8s endpoint) - pipeline, _ := setup(t) - - ctx := context.WithValue(context.Background(), "check-node-ready", true) - - // When executing node health check - err := pipeline.executeNodeHealthCheck(ctx) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "No health checks specified. Use --nodes and/or --k8s-endpoint flags to specify health checks to perform" { - t.Errorf("Expected health checks required error, got: %v", err) - } - }) - - t.Run("ReadyFlagFalse", func(t *testing.T) { - // Given a check pipeline with k8s endpoint and --ready=false - pipeline, mocks := setup(t) - - var capturedNodeNames []string - mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx context.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { - capturedNodeNames = nodeNames - return nil - } - - ctx := context.WithValue(context.Background(), "k8s-endpoint-provided", true) - ctx = context.WithValue(ctx, "k8s-endpoint", "") - ctx = context.WithValue(ctx, "check-node-ready", false) - ctx = context.WithValue(ctx, "output", func(msg string) {}) - - // When executing node health check - err := pipeline.executeNodeHealthCheck(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And no node names should be passed (ready=false) - if len(capturedNodeNames) != 0 { - t.Errorf("Expected no node names, got %v", capturedNodeNames) - } - }) - - t.Run("ReadyFlagNil", func(t *testing.T) { - // Given a check pipeline with k8s endpoint and no ready flag (nil) - pipeline, mocks := setup(t) - - var capturedNodeNames []string - mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx context.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { - capturedNodeNames = nodeNames - return nil - } - - ctx := context.WithValue(context.Background(), "k8s-endpoint-provided", true) - ctx = context.WithValue(ctx, "k8s-endpoint", "") - ctx = context.WithValue(ctx, "output", func(msg string) {}) - - // When executing node health check - err := pipeline.executeNodeHealthCheck(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And no node names should be passed (ready=nil) - if len(capturedNodeNames) != 0 { - t.Errorf("Expected no node names, got %v", capturedNodeNames) - } - }) - - t.Run("ShowsWaitingMessageWhenReadyFlagUsed", func(t *testing.T) { - // Given a check pipeline with --ready flag and specific nodes - pipeline, mocks := setup(t) - - var capturedMessages []string - outputFunc := func(msg string) { - capturedMessages = append(capturedMessages, msg) - } - - mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx context.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { - return nil - } - - ctx := context.WithValue(context.Background(), "k8s-endpoint-provided", true) - ctx = context.WithValue(ctx, "k8s-endpoint", "") - ctx = context.WithValue(ctx, "check-node-ready", true) - ctx = context.WithValue(ctx, "nodes", []string{"node1", "node2"}) - ctx = context.WithValue(ctx, "output", outputFunc) - - // When executing node health check - err := pipeline.executeNodeHealthCheck(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And waiting message should be shown - expectedMessage := "Waiting for 2 nodes to be Ready..." - found := false - for _, msg := range capturedMessages { - if msg == expectedMessage { - found = true - break - } - } - if !found { - t.Errorf("Expected waiting message '%s', got messages: %v", expectedMessage, capturedMessages) - } - }) - - t.Run("PassesOutputFuncToWaitForKubernetesHealthy", func(t *testing.T) { - // Given a check pipeline with --ready flag - pipeline, mocks := setup(t) - - var capturedOutputFunc func(string) - var capturedNodeNames []string - mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx context.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { - capturedOutputFunc = outputFunc - capturedNodeNames = nodeNames - return nil - } - - outputFunc := func(msg string) {} - ctx := context.WithValue(context.Background(), "k8s-endpoint-provided", true) - ctx = context.WithValue(ctx, "k8s-endpoint", "") - ctx = context.WithValue(ctx, "check-node-ready", true) - ctx = context.WithValue(ctx, "nodes", []string{"node1"}) - ctx = context.WithValue(ctx, "output", outputFunc) - - // When executing node health check - err := pipeline.executeNodeHealthCheck(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And output function should be passed to WaitForKubernetesHealthy - if capturedOutputFunc == nil { - t.Error("Expected output function to be passed to WaitForKubernetesHealthy") - } - - // And node names should be passed - if len(capturedNodeNames) != 1 || capturedNodeNames[0] != "node1" { - t.Errorf("Expected node names ['node1'], got %v", capturedNodeNames) - } - }) - - t.Run("NoImmediateNotFoundMessages", func(t *testing.T) { - // Given a check pipeline with --ready flag - pipeline, mocks := setup(t) - - var capturedMessages []string - outputFunc := func(msg string) { - capturedMessages = append(capturedMessages, msg) - } - - mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx context.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { - return nil - } - - ctx := context.WithValue(context.Background(), "k8s-endpoint-provided", true) - ctx = context.WithValue(ctx, "k8s-endpoint", "") - ctx = context.WithValue(ctx, "check-node-ready", true) - ctx = context.WithValue(ctx, "nodes", []string{"node1"}) - ctx = context.WithValue(ctx, "output", outputFunc) - - // When executing node health check - err := pipeline.executeNodeHealthCheck(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And no immediate "NOT FOUND" messages should be shown - for _, msg := range capturedMessages { - if strings.Contains(msg, "NOT FOUND") { - t.Errorf("Expected no immediate NOT FOUND messages, but found: %s", msg) - } - } - - // And the waiting message should be shown (along with Talos health check messages) - waitingMessageFound := false - for _, msg := range capturedMessages { - if msg == "Waiting for 1 nodes to be Ready..." { - waitingMessageFound = true - break - } - } - if !waitingMessageFound { - t.Errorf("Expected waiting message 'Waiting for 1 nodes to be Ready...', got messages: %v", capturedMessages) - } - }) - - t.Run("HandlesNilOutputFunc", func(t *testing.T) { - // Given a check pipeline with --ready flag but no output function - pipeline, mocks := setup(t) - - mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx context.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { - return nil - } - - ctx := context.WithValue(context.Background(), "k8s-endpoint-provided", true) - ctx = context.WithValue(ctx, "k8s-endpoint", "") - ctx = context.WithValue(ctx, "check-node-ready", true) - ctx = context.WithValue(ctx, "nodes", []string{"node1"}) - // No output function in context - - // When executing node health check - err := pipeline.executeNodeHealthCheck(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) -} diff --git a/pkg/pipelines/down.go b/pkg/pipelines/down.go deleted file mode 100644 index 6d5cb3ed4..000000000 --- a/pkg/pipelines/down.go +++ /dev/null @@ -1,260 +0,0 @@ -package pipelines - -import ( - "context" - "fmt" - "os" - "path/filepath" - - "github.com/windsorcli/cli/pkg/composer/blueprint" - envvars "github.com/windsorcli/cli/pkg/context/env" - "github.com/windsorcli/cli/pkg/context/shell" - "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/provisioner/kubernetes" - k8sclient "github.com/windsorcli/cli/pkg/provisioner/kubernetes/client" - terraforminfra "github.com/windsorcli/cli/pkg/provisioner/terraform" - "github.com/windsorcli/cli/pkg/workstation/network" - "github.com/windsorcli/cli/pkg/workstation/services" - "github.com/windsorcli/cli/pkg/workstation/virt" -) - -// The DownPipeline is a specialized component that manages the infrastructure teardown phase -// of the Windsor environment. It focuses on blueprint cleanup, stack teardown, container runtime -// shutdown, virtual machine shutdown, and optional cleanup of context-specific artifacts. -// The DownPipeline assumes that env pipeline has been executed to set up environment variables. - -// ============================================================================= -// Types -// ============================================================================= - -// DownPipeline provides infrastructure teardown functionality for the down command -type DownPipeline struct { - BasePipeline - virtualMachine virt.VirtualMachine - containerRuntime virt.ContainerRuntime - networkManager network.NetworkManager - stack terraforminfra.Stack - blueprintHandler blueprint.BlueprintHandler - kubernetesClient k8sclient.KubernetesClient - kubernetesManager kubernetes.KubernetesManager - envPrinters []envvars.EnvPrinter -} - -// ============================================================================= -// Constructor -// ============================================================================= - -// NewDownPipeline creates a new DownPipeline instance -func NewDownPipeline() *DownPipeline { - return &DownPipeline{ - BasePipeline: *NewBasePipeline(), - } -} - -// ============================================================================= -// Public Methods -// ============================================================================= - -// Initialize sets up the down pipeline components including virtual machine, -// container runtime, network manager, stack, and blueprint handler. It only initializes -// the components needed for the infrastructure teardown phase. -func (p *DownPipeline) Initialize(injector di.Injector, ctx context.Context) error { - if err := p.BasePipeline.Initialize(injector, ctx); err != nil { - return err - } - - p.virtualMachine = p.withVirtualMachine() - p.containerRuntime = p.withContainerRuntime() - p.networkManager = p.withNetworking() - p.stack = p.withStack() - p.kubernetesClient = p.withKubernetesClient() - p.kubernetesManager = p.withKubernetesManager() - - p.blueprintHandler = p.withBlueprintHandler() - - envPrinters, err := p.withEnvPrinters() - if err != nil { - return fmt.Errorf("failed to create env printers: %w", err) - } - p.envPrinters = envPrinters - - for _, envPrinter := range p.envPrinters { - if err := envPrinter.Initialize(); err != nil { - return fmt.Errorf("failed to initialize env printer: %w", err) - } - } - - if p.virtualMachine != nil { - if err := p.virtualMachine.Initialize(); err != nil { - return fmt.Errorf("failed to initialize virtual machine: %w", err) - } - } - if p.containerRuntime != nil { - if err := p.containerRuntime.Initialize(); err != nil { - return fmt.Errorf("failed to initialize container runtime: %w", err) - } - } - - if secureShell := p.injector.Resolve("secureShell"); secureShell != nil { - if secureShellInterface, ok := secureShell.(shell.Shell); ok { - if err := secureShellInterface.Initialize(); err != nil { - return fmt.Errorf("failed to initialize secure shell: %w", err) - } - } - } - - if p.networkManager != nil { - resolvedServices, _ := p.injector.ResolveAll(new(services.Service)) - serviceList := make([]services.Service, 0, len(resolvedServices)) - for _, svc := range resolvedServices { - if s, ok := svc.(services.Service); ok { - serviceList = append(serviceList, s) - } - } - if err := p.networkManager.Initialize(serviceList); err != nil { - return fmt.Errorf("failed to initialize network manager: %w", err) - } - } - if p.stack != nil { - if err := p.stack.Initialize(); err != nil { - return fmt.Errorf("failed to initialize stack: %w", err) - } - } - if p.kubernetesManager != nil { - if err := p.kubernetesManager.Initialize(); err != nil { - return fmt.Errorf("failed to initialize kubernetes manager: %w", err) - } - } - if p.blueprintHandler != nil { - if err := p.blueprintHandler.Initialize(); err != nil { - return fmt.Errorf("failed to initialize blueprint handler: %w", err) - } - } - - return nil -} - -// Execute runs the down pipeline, performing infrastructure teardown in reverse order: -// 1. Set environment variables globally in the process -// 2. Run blueprint cleanup (if not skipped) -// 3. Tear down the stack (if not skipped) -// 4. Tear down container runtime (if enabled) -// 5. Clean up context-specific artifacts (if clean flag is set) -func (p *DownPipeline) Execute(ctx context.Context) error { - // Run blueprint cleanup before stack down (unless skipped) - skipK8sFlag := ctx.Value("skipK8s") - if skipK8sFlag == nil || !skipK8sFlag.(bool) { - if p.blueprintHandler == nil { - return fmt.Errorf("No blueprint handler found") - } - if err := p.blueprintHandler.LoadConfig(); err != nil { - return fmt.Errorf("Error loading blueprint config: %w", err) - } - if err := p.blueprintHandler.Down(); err != nil { - return fmt.Errorf("Error running blueprint down: %w", err) - } - } else { - fmt.Fprintln(os.Stderr, "Skipping Kubernetes cleanup (--skip-k8s set)") - } - - // Tear down the stack components (unless skipped) - skipTerraformFlag := ctx.Value("skipTerraform") - if skipTerraformFlag == nil || !skipTerraformFlag.(bool) { - if p.stack == nil { - return fmt.Errorf("No stack found") - } - if p.blueprintHandler == nil { - return fmt.Errorf("No blueprint handler found") - } - // Load blueprint config if not already loaded (e.g., if skipK8s was true) - if err := p.blueprintHandler.LoadConfig(); err != nil { - return fmt.Errorf("Error loading blueprint config: %w", err) - } - if err := p.blueprintHandler.LoadBlueprint(); err != nil { - return fmt.Errorf("Error loading blueprint: %w", err) - } - blueprint := p.blueprintHandler.Generate() - if err := p.stack.Down(blueprint); err != nil { - return fmt.Errorf("Error running stack Down command: %w", err) - } - } else { - fmt.Fprintln(os.Stderr, "Skipping Terraform cleanup (--skip-tf set)") - } - - // Tear down the container runtime if enabled - containerRuntimeEnabled := p.configHandler.GetBool("docker.enabled") - skipDockerFlag := ctx.Value("skipDocker") - if containerRuntimeEnabled && (skipDockerFlag == nil || !skipDockerFlag.(bool)) { - if p.containerRuntime == nil { - return fmt.Errorf("No container runtime found") - } - if err := p.containerRuntime.Down(); err != nil { - return fmt.Errorf("Error running container runtime Down command: %w", err) - } - } else if skipDockerFlag != nil && skipDockerFlag.(bool) { - fmt.Fprintln(os.Stderr, "Skipping Docker container cleanup (--skip-docker set)") - } - - // Clean up context specific artifacts if --clean flag is set - cleanFlag := ctx.Value("clean") - if cleanFlag != nil && cleanFlag.(bool) { - if err := p.performCleanup(); err != nil { - return fmt.Errorf("Error performing cleanup: %w", err) - } - } - - // Print success message - fmt.Fprintln(os.Stderr, "Windsor environment torn down successfully.") - - return nil -} - -// ============================================================================= -// Private Methods -// ============================================================================= - -// performCleanup performs cleanup of context-specific artifacts including -// configuration cleanup, volumes folder removal, terraform modules removal, -// and generated files removal. -func (p *DownPipeline) performCleanup() error { - if err := p.configHandler.Clean(); err != nil { - return fmt.Errorf("Error cleaning up context specific artifacts: %w", err) - } - - projectRoot, err := p.shell.GetProjectRoot() - if err != nil { - return fmt.Errorf("Error retrieving project root: %w", err) - } - - // Delete everything in the .volumes folder - volumesPath := filepath.Join(projectRoot, ".volumes") - if err := p.shims.RemoveAll(volumesPath); err != nil { - return fmt.Errorf("Error deleting .volumes folder: %w", err) - } - - // Delete the .windsor/.tf_modules folder - tfModulesPath := filepath.Join(projectRoot, ".windsor", ".tf_modules") - if err := p.shims.RemoveAll(tfModulesPath); err != nil { - return fmt.Errorf("Error deleting .windsor/.tf_modules folder: %w", err) - } - - // Delete .windsor/Corefile - corefilePath := filepath.Join(projectRoot, ".windsor", "Corefile") - if err := p.shims.RemoveAll(corefilePath); err != nil { - return fmt.Errorf("Error deleting .windsor/Corefile: %w", err) - } - - // Delete .windsor/docker-compose.yaml - dockerComposePath := filepath.Join(projectRoot, ".windsor", "docker-compose.yaml") - if err := p.shims.RemoveAll(dockerComposePath); err != nil { - return fmt.Errorf("Error deleting .windsor/docker-compose.yaml: %w", err) - } - - return nil -} - -// ============================================================================= -// Interface Compliance -// ============================================================================= - -var _ Pipeline = (*DownPipeline)(nil) diff --git a/pkg/pipelines/down_test.go b/pkg/pipelines/down_test.go deleted file mode 100644 index 0a39e81b8..000000000 --- a/pkg/pipelines/down_test.go +++ /dev/null @@ -1,1182 +0,0 @@ -package pipelines - -import ( - "context" - "fmt" - "path/filepath" - "strings" - "testing" - - blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" - "github.com/windsorcli/cli/pkg/composer/blueprint" - "github.com/windsorcli/cli/pkg/context/config" - envvars "github.com/windsorcli/cli/pkg/context/env" - "github.com/windsorcli/cli/pkg/context/shell" - "github.com/windsorcli/cli/pkg/provisioner/kubernetes" - terraforminfra "github.com/windsorcli/cli/pkg/provisioner/terraform" - "github.com/windsorcli/cli/pkg/workstation/network" - "github.com/windsorcli/cli/pkg/workstation/services" - "github.com/windsorcli/cli/pkg/workstation/virt" -) - -// ============================================================================= -// Test Setup -// ============================================================================= - -type DownMocks struct { - *Mocks - VirtualMachine *virt.MockVirt - ContainerRuntime *virt.MockVirt - NetworkManager *network.MockNetworkManager - Stack *terraforminfra.MockStack - BlueprintHandler *blueprint.MockBlueprintHandler -} - -func setupDownMocks(t *testing.T, opts ...*SetupOptions) *DownMocks { - t.Helper() - - // Create setup options, preserving any provided options - setupOptions := &SetupOptions{} - if len(opts) > 0 && opts[0] != nil { - setupOptions = opts[0] - } - - baseMocks := setupMocks(t, setupOptions) - - // Add down-specific shell mock behaviors - baseMocks.Shell.GetSessionTokenFunc = func() (string, error) { return "mock-session-token", nil } - - // Initialize the config handler if it's a real one - if setupOptions.ConfigHandler == nil { - configHandler := baseMocks.ConfigHandler - configHandler.SetContext("mock-context") - - // Load base config with down-specific settings - configYAML := ` -apiVersion: v1alpha1 -contexts: - mock-context: - dns: - domain: mock.domain.com - enabled: true - network: - cidr_block: 10.0.0.0/24 - docker: - enabled: true - vm: - driver: colima` - - if err := configHandler.LoadConfigString(configYAML); err != nil { - t.Fatalf("Failed to load config: %v", err) - } - } - - // Setup virtual machine mock - mockVirtualMachine := virt.NewMockVirt() - mockVirtualMachine.InitializeFunc = func() error { return nil } - mockVirtualMachine.DownFunc = func() error { return nil } - baseMocks.Injector.Register("virtualMachine", mockVirtualMachine) - - // Setup container runtime mock - mockContainerRuntime := virt.NewMockVirt() - mockContainerRuntime.InitializeFunc = func() error { return nil } - mockContainerRuntime.DownFunc = func() error { return nil } - baseMocks.Injector.Register("containerRuntime", mockContainerRuntime) - - // Setup network manager mock - mockNetworkManager := network.NewMockNetworkManager() - mockNetworkManager.InitializeFunc = func([]services.Service) error { return nil } - baseMocks.Injector.Register("networkManager", mockNetworkManager) - - // Setup stack mock - mockStack := terraforminfra.NewMockStack(baseMocks.Injector) - mockStack.InitializeFunc = func() error { return nil } - mockStack.DownFunc = func(blueprint *blueprintv1alpha1.Blueprint) error { return nil } - baseMocks.Injector.Register("stack", mockStack) - - // Setup blueprint handler mock - mockBlueprintHandler := blueprint.NewMockBlueprintHandler(baseMocks.Injector) - mockBlueprintHandler.InitializeFunc = func() error { return nil } - mockBlueprintHandler.LoadConfigFunc = func() error { return nil } - mockBlueprintHandler.DownFunc = func() error { return nil } - baseMocks.Injector.Register("blueprintHandler", mockBlueprintHandler) - - // Setup env printers - mockEnvPrinters := []envvars.EnvPrinter{} - windsorEnv := envvars.NewMockEnvPrinter() - windsorEnv.InitializeFunc = func() error { return nil } - windsorEnv.GetEnvVarsFunc = func() (map[string]string, error) { - return map[string]string{"WINDSOR_TEST": "true"}, nil - } - mockEnvPrinters = append(mockEnvPrinters, windsorEnv) - baseMocks.Injector.Register("windsorEnv", windsorEnv) - - return &DownMocks{ - Mocks: baseMocks, - VirtualMachine: mockVirtualMachine, - ContainerRuntime: mockContainerRuntime, - NetworkManager: mockNetworkManager, - Stack: mockStack, - BlueprintHandler: mockBlueprintHandler, - } -} - -// ============================================================================= -// Constructor Tests -// ============================================================================= - -func TestNewDownPipeline(t *testing.T) { - t.Run("Success", func(t *testing.T) { - // When creating a new down pipeline - pipeline := NewDownPipeline() - - // Then it should not be nil - if pipeline == nil { - t.Error("Expected pipeline to be non-nil") - } - }) -} - -// ============================================================================= -// Initialize Tests -// ============================================================================= - -func TestDownPipeline_Initialize(t *testing.T) { - t.Run("Success", func(t *testing.T) { - // Given a down pipeline and mocks - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And all components should be initialized - if pipeline.virtualMachine == nil { - t.Error("Expected virtual machine to be initialized") - } - if pipeline.containerRuntime == nil { - t.Error("Expected container runtime to be initialized") - } - if pipeline.networkManager == nil { - t.Error("Expected network manager to be initialized") - } - if pipeline.stack == nil { - t.Error("Expected stack to be initialized") - } - if pipeline.blueprintHandler == nil { - t.Error("Expected blueprint handler to be initialized") - } - if len(pipeline.envPrinters) == 0 { - t.Error("Expected env printers to be initialized") - } - }) - - t.Run("InitializesSecureShellWhenRegistered", func(t *testing.T) { - // Given a down pipeline with secure shell registered - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - - // Create mock secure shell - mockSecureShell := shell.NewMockShell() - secureShellInitialized := false - mockSecureShell.InitializeFunc = func() error { - secureShellInitialized = true - return nil - } - mocks.Injector.Register("secureShell", mockSecureShell) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And secure shell should be initialized - if !secureShellInitialized { - t.Error("Expected secure shell to be initialized") - } - }) - - t.Run("ReturnsErrorWhenSecureShellInitializeFails", func(t *testing.T) { - // Given a down pipeline with failing secure shell - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - - // Create mock secure shell that fails to initialize - mockSecureShell := shell.NewMockShell() - mockSecureShell.InitializeFunc = func() error { - return fmt.Errorf("secure shell failed") - } - mocks.Injector.Register("secureShell", mockSecureShell) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "failed to initialize secure shell: secure shell failed" { - t.Errorf("Expected secure shell error, got %q", err.Error()) - } - }) - - t.Run("SkipsSecureShellWhenNotRegistered", func(t *testing.T) { - // Given a down pipeline without secure shell registered - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("SkipsSecureShellWhenRegisteredTypeIsIncorrect", func(t *testing.T) { - // Given a down pipeline with incorrectly typed secure shell - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - - // Register something that's not a shell.Shell - mocks.Injector.Register("secureShell", "not-a-shell") - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("ErrorInitializingBasePipeline", func(t *testing.T) { - // Given a down pipeline with failing base pipeline - pipeline := NewDownPipeline() - - // Create a mock config handler that succeeds during setup but fails during pipeline init - initCallCount := 0 - failingConfigHandler := &config.MockConfigHandler{ - InitializeFunc: func() error { - initCallCount++ - if initCallCount > 1 { - return fmt.Errorf("config initialization failed") - } - return nil - }, - SetContextFunc: func(context string) error { return nil }, - LoadConfigStringFunc: func(configString string) error { return nil }, - } - - mocks := setupDownMocks(t, &SetupOptions{ - ConfigHandler: failingConfigHandler, - }) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - return - } - - if !strings.Contains(err.Error(), "config initialization failed") { - t.Errorf("Expected error message containing 'config initialization failed', got: %v", err) - } - }) - - t.Run("ErrorInitializingEnvPrinters", func(t *testing.T) { - // Given a down pipeline with failing env printer initialization - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - - // Initialize the base pipeline first - err := pipeline.BasePipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize base pipeline: %v", err) - } - - // Create a failing env printer and register it - failingEnvPrinter := envvars.NewMockEnvPrinter() - failingEnvPrinter.InitializeFunc = func() error { - return fmt.Errorf("env printer initialization failed") - } - - // Set the env printers directly to include the failing one - pipeline.envPrinters = []envvars.EnvPrinter{failingEnvPrinter} - - // When initializing the env printers - var initErr error - for _, envPrinter := range pipeline.envPrinters { - if err := envPrinter.Initialize(); err != nil { - initErr = fmt.Errorf("failed to initialize env printer: %w", err) - break - } - } - - // Then an error should be returned - if initErr == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(initErr.Error(), "env printer initialization failed") { - t.Errorf("Expected error message containing 'env printer initialization failed', got: %v", initErr) - } - }) - - t.Run("ErrorInitializingVirtualMachine", func(t *testing.T) { - // Given a down pipeline with failing virtual machine initialization - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - - // Set up a failing virtual machine mock - mocks.VirtualMachine.InitializeFunc = func() error { - return fmt.Errorf("virtual machine initialization failed") - } - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to initialize virtual machine") { - t.Errorf("Expected error message containing 'failed to initialize virtual machine', got: %v", err) - } - }) - - t.Run("ErrorInitializingContainerRuntime", func(t *testing.T) { - // Given a down pipeline with failing container runtime initialization - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - - // Set up a failing container runtime mock - mocks.ContainerRuntime.InitializeFunc = func() error { - return fmt.Errorf("container runtime initialization failed") - } - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to initialize container runtime") { - t.Errorf("Expected error message containing 'failed to initialize container runtime', got: %v", err) - } - }) - - t.Run("ErrorInitializingNetworkManager", func(t *testing.T) { - // Given a down pipeline with failing network manager initialization - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - - // Set up a failing network manager mock - mocks.NetworkManager.InitializeFunc = func([]services.Service) error { - return fmt.Errorf("network manager initialization failed") - } - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to initialize network manager") { - t.Errorf("Expected error message containing 'failed to initialize network manager', got: %v", err) - } - }) - - t.Run("ErrorInitializingStack", func(t *testing.T) { - // Given a down pipeline with failing stack initialization - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - - // Set up a failing stack mock - mocks.Stack.InitializeFunc = func() error { - return fmt.Errorf("stack initialization failed") - } - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to initialize stack") { - t.Errorf("Expected error message containing 'failed to initialize stack', got: %v", err) - } - }) - - t.Run("ErrorInitializingBlueprintHandler", func(t *testing.T) { - // Given a down pipeline with failing blueprint handler initialization - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - - // Set up a failing blueprint handler mock - mocks.BlueprintHandler.InitializeFunc = func() error { - return fmt.Errorf("blueprint handler initialization failed") - } - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to initialize blueprint handler") { - t.Errorf("Expected error message containing 'failed to initialize blueprint handler', got: %v", err) - } - }) - - t.Run("ErrorInitializingKubernetesManager", func(t *testing.T) { - // Given a down pipeline with failing kubernetes manager initialization - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - - // Create a failing kubernetes manager mock - failingKubernetesManager := kubernetes.NewMockKubernetesManager(mocks.Injector) - failingKubernetesManager.InitializeFunc = func() error { - return fmt.Errorf("kubernetes manager initialization failed") - } - - // Register the failing kubernetes manager - mocks.Injector.Register("kubernetesManager", failingKubernetesManager) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to initialize kubernetes manager") { - t.Errorf("Expected error message containing 'failed to initialize kubernetes manager', got: %v", err) - } - }) -} - -// ============================================================================= -// Execute Tests -// ============================================================================= - -func TestDownPipeline_Execute(t *testing.T) { - t.Run("Success", func(t *testing.T) { - // Given a down pipeline and mocks - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Track method calls - var blueprintDownCalled bool - var stackDownCalled bool - var containerRuntimeDownCalled bool - - mocks.BlueprintHandler.DownFunc = func() error { - blueprintDownCalled = true - return nil - } - mocks.Stack.DownFunc = func(blueprint *blueprintv1alpha1.Blueprint) error { - stackDownCalled = true - return nil - } - mocks.ContainerRuntime.DownFunc = func() error { - containerRuntimeDownCalled = true - return nil - } - - // When executing the pipeline - err = pipeline.Execute(context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And blueprint down should be called - if !blueprintDownCalled { - t.Error("Expected blueprint down to be called") - } - - // And stack down should be called - if !stackDownCalled { - t.Error("Expected stack down to be called") - } - - // And container runtime down should be called - if !containerRuntimeDownCalled { - t.Error("Expected container runtime down to be called") - } - }) - - t.Run("SkipK8sFlag", func(t *testing.T) { - // Given a down pipeline and mocks - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Track method calls - var blueprintDownCalled bool - var stackDownCalled bool - - mocks.BlueprintHandler.DownFunc = func() error { - blueprintDownCalled = true - return nil - } - mocks.Stack.DownFunc = func(blueprint *blueprintv1alpha1.Blueprint) error { - stackDownCalled = true - return nil - } - - // When executing the pipeline with skipK8s flag - ctx := context.WithValue(context.Background(), "skipK8s", true) - err = pipeline.Execute(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And blueprint down should NOT be called - if blueprintDownCalled { - t.Error("Expected blueprint down to NOT be called when skipK8s is true") - } - - // And stack down should still be called - if !stackDownCalled { - t.Error("Expected stack down to be called") - } - }) - - t.Run("SkipTerraformFlag", func(t *testing.T) { - // Given a down pipeline and mocks - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Track method calls - var blueprintDownCalled bool - var stackDownCalled bool - - mocks.BlueprintHandler.DownFunc = func() error { - blueprintDownCalled = true - return nil - } - mocks.Stack.DownFunc = func(blueprint *blueprintv1alpha1.Blueprint) error { - stackDownCalled = true - return nil - } - - // When executing the pipeline with skipTerraform flag - ctx := context.WithValue(context.Background(), "skipTerraform", true) - err = pipeline.Execute(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And blueprint down should be called - if !blueprintDownCalled { - t.Error("Expected blueprint down to be called") - } - - // And stack down should NOT be called - if stackDownCalled { - t.Error("Expected stack down to NOT be called when skipTerraform is true") - } - }) - - t.Run("CleanFlag", func(t *testing.T) { - // Given a down pipeline with mock config handler - mockConfigHandler := &config.MockConfigHandler{ - InitializeFunc: func() error { return nil }, - SetContextFunc: func(context string) error { return nil }, - LoadConfigStringFunc: func(configString string) error { return nil }, - GetBoolFunc: func(key string, defaultValue ...bool) bool { - switch key { - case "docker.enabled": - return true - default: - return false - } - }, - GetStringFunc: func(key string, defaultValue ...string) string { - return "" - }, - } - - // Track config clean calls - var configCleanCalled bool - mockConfigHandler.CleanFunc = func() error { - configCleanCalled = true - return nil - } - - pipeline := NewDownPipeline() - mocks := setupDownMocks(t, &SetupOptions{ - ConfigHandler: mockConfigHandler, - }) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Setup shell mock to return project root - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "/test/project", nil - } - - // Track removed paths - var removedPaths []string - pipeline.shims.RemoveAll = func(path string) error { - removedPaths = append(removedPaths, path) - return nil - } - - // When executing the pipeline with clean flag - ctx := context.WithValue(context.Background(), "clean", true) - err = pipeline.Execute(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And cleanup should be performed - if !configCleanCalled { - t.Error("Expected config handler clean to be called") - } - - // And specific paths should be removed - expectedPaths := []string{ - filepath.Join("/test/project", ".volumes"), - filepath.Join("/test/project", ".windsor", ".tf_modules"), - filepath.Join("/test/project", ".windsor", "Corefile"), - filepath.Join("/test/project", ".windsor", "docker-compose.yaml"), - } - - if len(removedPaths) != len(expectedPaths) { - t.Errorf("Expected %d paths to be removed, got %d", len(expectedPaths), len(removedPaths)) - } - - for i, expectedPath := range expectedPaths { - if i >= len(removedPaths) || removedPaths[i] != expectedPath { - t.Errorf("Expected path %s to be removed, got %v", expectedPath, removedPaths) - } - } - }) - - t.Run("ErrorBlueprintDown", func(t *testing.T) { - // Given a down pipeline with failing blueprint handler - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - mocks.BlueprintHandler.DownFunc = func() error { - return fmt.Errorf("blueprint down failed") - } - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When executing the pipeline - err = pipeline.Execute(context.Background()) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if err.Error() != "Error running blueprint down: blueprint down failed" { - t.Errorf("Expected specific error message, got: %v", err) - } - }) - - t.Run("ErrorStackDown", func(t *testing.T) { - // Given a down pipeline with failing stack - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - mocks.Stack.DownFunc = func(blueprint *blueprintv1alpha1.Blueprint) error { - return fmt.Errorf("stack down failed") - } - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When executing the pipeline - err = pipeline.Execute(context.Background()) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if err.Error() != "Error running stack Down command: stack down failed" { - t.Errorf("Expected specific error message, got: %v", err) - } - }) - - t.Run("ErrorContainerRuntimeDown", func(t *testing.T) { - // Given a down pipeline with failing container runtime - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - mocks.ContainerRuntime.DownFunc = func() error { - return fmt.Errorf("container runtime down failed") - } - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When executing the pipeline - err = pipeline.Execute(context.Background()) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if err.Error() != "Error running container runtime Down command: container runtime down failed" { - t.Errorf("Expected specific error message, got: %v", err) - } - }) - - t.Run("ErrorLoadingBlueprintConfig", func(t *testing.T) { - // Given a down pipeline with failing blueprint config loading - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - mocks.BlueprintHandler.LoadConfigFunc = func() error { - return fmt.Errorf("failed to load blueprint config") - } - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When executing the pipeline - err = pipeline.Execute(context.Background()) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "Error loading blueprint config") { - t.Errorf("Expected error message containing 'Error loading blueprint config', got: %v", err) - } - }) - - t.Run("MissingBlueprintHandler", func(t *testing.T) { - // Given a down pipeline without blueprint handler - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Set blueprint handler to nil - pipeline.blueprintHandler = nil - - // When executing the pipeline - err = pipeline.Execute(context.Background()) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "No blueprint handler found") { - t.Errorf("Expected error message containing 'No blueprint handler found', got: %v", err) - } - }) - - t.Run("MissingStack", func(t *testing.T) { - // Given a down pipeline without stack - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Set stack to nil - pipeline.stack = nil - - // When executing the pipeline - err = pipeline.Execute(context.Background()) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "No stack found") { - t.Errorf("Expected error message containing 'No stack found', got: %v", err) - } - }) - - t.Run("MissingContainerRuntime", func(t *testing.T) { - // Given a down pipeline without container runtime but docker enabled - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Set container runtime to nil - pipeline.containerRuntime = nil - - // When executing the pipeline - err = pipeline.Execute(context.Background()) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "No container runtime found") { - t.Errorf("Expected error message containing 'No container runtime found', got: %v", err) - } - }) - - t.Run("ErrorDuringCleanup", func(t *testing.T) { - // Given a down pipeline with failing cleanup - mockConfigHandler := &config.MockConfigHandler{ - InitializeFunc: func() error { return nil }, - SetContextFunc: func(context string) error { return nil }, - LoadConfigStringFunc: func(configString string) error { return nil }, - GetBoolFunc: func(key string, defaultValue ...bool) bool { - switch key { - case "docker.enabled": - return true - default: - return false - } - }, - GetStringFunc: func(key string, defaultValue ...string) string { - return "" - }, - CleanFunc: func() error { - return fmt.Errorf("cleanup failed") - }, - } - - pipeline := NewDownPipeline() - mocks := setupDownMocks(t, &SetupOptions{ - ConfigHandler: mockConfigHandler, - }) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When executing the pipeline with clean flag - ctx := context.WithValue(context.Background(), "clean", true) - err = pipeline.Execute(ctx) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "Error performing cleanup") { - t.Errorf("Expected error message containing 'Error performing cleanup', got: %v", err) - } - }) - - t.Run("SkipDockerFlag", func(t *testing.T) { - // Given a down pipeline with skipDocker flag set - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Track method calls - var blueprintDownCalled bool - var stackDownCalled bool - var containerRuntimeDownCalled bool - - mocks.BlueprintHandler.DownFunc = func() error { - blueprintDownCalled = true - return nil - } - mocks.Stack.DownFunc = func(blueprint *blueprintv1alpha1.Blueprint) error { - stackDownCalled = true - return nil - } - mocks.ContainerRuntime.DownFunc = func() error { - containerRuntimeDownCalled = true - return nil - } - - // Create context with skipDocker flag - ctx := context.WithValue(context.Background(), "skipDocker", true) - - // When executing the pipeline - err = pipeline.Execute(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And blueprint down should be called - if !blueprintDownCalled { - t.Error("Expected blueprint down to be called") - } - - // And stack down should be called - if !stackDownCalled { - t.Error("Expected stack down to be called") - } - - // And container runtime down should NOT be called - if containerRuntimeDownCalled { - t.Error("Expected container runtime down to NOT be called") - } - }) -} - -// ============================================================================= -// performCleanup Tests -// ============================================================================= - -func TestDownPipeline_performCleanup(t *testing.T) { - t.Run("Success", func(t *testing.T) { - // Given a down pipeline with working cleanup using mock config handler - mockConfigHandler := &config.MockConfigHandler{ - InitializeFunc: func() error { return nil }, - SetContextFunc: func(context string) error { return nil }, - LoadConfigStringFunc: func(configString string) error { return nil }, - GetBoolFunc: func(key string, defaultValue ...bool) bool { - switch key { - case "docker.enabled": - return true - default: - return false - } - }, - GetStringFunc: func(key string, defaultValue ...string) string { - return "" - }, - } - - pipeline := NewDownPipeline() - mocks := setupDownMocks(t, &SetupOptions{ - ConfigHandler: mockConfigHandler, - }) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Track cleanup calls - var configCleanCalled bool - var removeAllCalls []string - - mockConfigHandler.CleanFunc = func() error { - configCleanCalled = true - return nil - } - - mocks.Shims.RemoveAll = func(path string) error { - removeAllCalls = append(removeAllCalls, path) - return nil - } - - // When performing cleanup - err = pipeline.performCleanup() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And config clean should be called - if !configCleanCalled { - t.Error("Expected config clean to be called") - } - - // And all expected paths should be removed (4 calls expected) - expectedCallCount := 4 - if len(removeAllCalls) != expectedCallCount { - t.Errorf("Expected %d RemoveAll calls, got %d", expectedCallCount, len(removeAllCalls)) - } - - // Check that the expected path suffixes are present - expectedSuffixes := []string{ - ".volumes", - filepath.Join(".windsor", ".tf_modules"), - filepath.Join(".windsor", "Corefile"), - filepath.Join(".windsor", "docker-compose.yaml"), - } - - for i, expectedSuffix := range expectedSuffixes { - if i < len(removeAllCalls) && !strings.HasSuffix(removeAllCalls[i], expectedSuffix) { - t.Errorf("Expected RemoveAll call %d to end with %s, got %s", i, expectedSuffix, removeAllCalls[i]) - } - } - }) - - t.Run("ErrorGettingProjectRoot", func(t *testing.T) { - // Given a down pipeline with failing project root retrieval - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Make GetProjectRoot fail - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("failed to get project root") - } - - // When performing cleanup - err = pipeline.performCleanup() - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to get project root") { - t.Errorf("Expected error message containing 'failed to get project root', got: %v", err) - } - }) - - t.Run("ErrorRemovingVolumesFolder", func(t *testing.T) { - // Given a down pipeline with failing volumes folder removal - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Make RemoveAll fail for volumes folder - mocks.Shims.RemoveAll = func(path string) error { - if strings.HasSuffix(path, ".volumes") { - return fmt.Errorf("failed to remove volumes folder") - } - return nil - } - - // When performing cleanup - err = pipeline.performCleanup() - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "Error deleting .volumes folder") { - t.Errorf("Expected error message containing 'Error deleting .volumes folder', got: %v", err) - } - }) - - t.Run("ErrorRemovingTfModulesFolder", func(t *testing.T) { - // Given a down pipeline with failing tf modules folder removal - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Make RemoveAll fail for tf modules folder - mocks.Shims.RemoveAll = func(path string) error { - if strings.HasSuffix(path, ".tf_modules") { - return fmt.Errorf("failed to remove tf modules folder") - } - return nil - } - - // When performing cleanup - err = pipeline.performCleanup() - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "Error deleting .windsor/.tf_modules folder") { - t.Errorf("Expected error message containing 'Error deleting .windsor/.tf_modules folder', got: %v", err) - } - }) - - t.Run("ErrorRemovingCorefile", func(t *testing.T) { - // Given a down pipeline with failing Corefile removal - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Make RemoveAll fail for Corefile - mocks.Shims.RemoveAll = func(path string) error { - if strings.HasSuffix(path, "Corefile") { - return fmt.Errorf("failed to remove Corefile") - } - return nil - } - - // When performing cleanup - err = pipeline.performCleanup() - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "Error deleting .windsor/Corefile") { - t.Errorf("Expected error message containing 'Error deleting .windsor/Corefile', got: %v", err) - } - }) - - t.Run("ErrorRemovingDockerCompose", func(t *testing.T) { - // Given a down pipeline with failing docker-compose removal - pipeline := NewDownPipeline() - mocks := setupDownMocks(t) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Make RemoveAll fail for docker-compose.yaml - mocks.Shims.RemoveAll = func(path string) error { - if strings.HasSuffix(path, "docker-compose.yaml") { - return fmt.Errorf("failed to remove docker-compose.yaml") - } - return nil - } - - // When performing cleanup - err = pipeline.performCleanup() - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "Error deleting .windsor/docker-compose.yaml") { - t.Errorf("Expected error message containing 'Error deleting .windsor/docker-compose.yaml', got: %v", err) - } - }) -} diff --git a/pkg/pipelines/exec.go b/pkg/pipelines/exec.go deleted file mode 100644 index 52bc3757f..000000000 --- a/pkg/pipelines/exec.go +++ /dev/null @@ -1,62 +0,0 @@ -package pipelines - -import ( - "context" - "fmt" -) - -// The ExecPipeline is a specialized component that manages command execution with environment injection. -// It collects environment variables from all configured env printers and sets them in the process -// environment before executing commands, ensuring the same environment variables are injected -// that would be printed by the windsor env command. - -// ============================================================================= -// Types -// ============================================================================= - -// ExecPipeline provides command execution functionality with environment injection -type ExecPipeline struct { - BasePipeline -} - -// ============================================================================= -// Constructor -// ============================================================================= - -// NewExecPipeline creates a new ExecPipeline instance -func NewExecPipeline() *ExecPipeline { - return &ExecPipeline{ - BasePipeline: *NewBasePipeline(), - } -} - -// ============================================================================= -// Public Methods -// ============================================================================= - -// Execute executes the command with the provided arguments. -// It expects the command and optional arguments to be provided in the context. -func (p *ExecPipeline) Execute(ctx context.Context) error { - command, ok := ctx.Value("command").(string) - if !ok || command == "" { - return fmt.Errorf("no command provided in context") - } - - var args []string - if ctxArgs := ctx.Value("args"); ctxArgs != nil { - args = ctxArgs.([]string) - } - - _, err := p.shell.Exec(command, args...) - if err != nil { - return fmt.Errorf("command execution failed: %w", err) - } - - return nil -} - -// ============================================================================= -// Interface Compliance -// ============================================================================= - -var _ Pipeline = (*ExecPipeline)(nil) diff --git a/pkg/pipelines/exec_test.go b/pkg/pipelines/exec_test.go deleted file mode 100644 index 31e8a5472..000000000 --- a/pkg/pipelines/exec_test.go +++ /dev/null @@ -1,240 +0,0 @@ -package pipelines - -import ( - "context" - "fmt" - "testing" - - "github.com/windsorcli/cli/pkg/context/config" -) - -// ============================================================================= -// Test Setup -// ============================================================================= - -type ExecMocks struct { - *Mocks -} - -func setupExecMocks(t *testing.T, opts ...*SetupOptions) *ExecMocks { - t.Helper() - - // Create setup options, preserving any provided options - setupOptions := &SetupOptions{} - if len(opts) > 0 && opts[0] != nil { - setupOptions = opts[0] - } - - // Only create a default mock config handler if one wasn't provided - if setupOptions.ConfigHandler == nil { - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.InitializeFunc = func() error { return nil } - mockConfigHandler.IsLoadedFunc = func() bool { return true } // Default to loaded - setupOptions.ConfigHandler = mockConfigHandler - } - - baseMocks := setupMocks(t, setupOptions) - - return &ExecMocks{ - Mocks: baseMocks, - } -} - -// ============================================================================= -// Test Constructor -// ============================================================================= - -func TestNewExecPipeline(t *testing.T) { - t.Run("CreatesWithDefaults", func(t *testing.T) { - // Given creating a new exec pipeline - pipeline := NewExecPipeline() - - // Then pipeline should not be nil - if pipeline == nil { - t.Fatal("Expected pipeline to not be nil") - } - }) -} - -// ============================================================================= -// Test Public Methods -// ============================================================================= - -func TestExecPipeline_Execute(t *testing.T) { - setup := func(t *testing.T, opts ...*SetupOptions) (*ExecPipeline, *ExecMocks) { - t.Helper() - pipeline := NewExecPipeline() - mocks := setupExecMocks(t, opts...) - - // Initialize the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - return pipeline, mocks - } - - t.Run("ExecutesCommandSuccessfully", func(t *testing.T) { - // Given an exec pipeline with a mock shell - pipeline, mocks := setup(t) - - execCalled := false - var capturedCommand string - var capturedArgs []string - mocks.Shell.ExecFunc = func(command string, args ...string) (string, error) { - execCalled = true - capturedCommand = command - capturedArgs = args - return "command output", nil - } - - ctx := context.WithValue(context.Background(), "command", "test-command") - ctx = context.WithValue(ctx, "args", []string{"arg1", "arg2"}) - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then no error should be returned and command should be executed - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if !execCalled { - t.Error("Expected shell.Exec to be called") - } - if capturedCommand != "test-command" { - t.Errorf("Expected command 'test-command', got '%s'", capturedCommand) - } - if len(capturedArgs) != 2 || capturedArgs[0] != "arg1" || capturedArgs[1] != "arg2" { - t.Errorf("Expected args ['arg1', 'arg2'], got %v", capturedArgs) - } - }) - - t.Run("ExecutesCommandWithoutArgs", func(t *testing.T) { - // Given an exec pipeline with a mock shell and no args - pipeline, mocks := setup(t) - - execCalled := false - var capturedCommand string - var capturedArgs []string - mocks.Shell.ExecFunc = func(command string, args ...string) (string, error) { - execCalled = true - capturedCommand = command - capturedArgs = args - return "command output", nil - } - - ctx := context.WithValue(context.Background(), "command", "test-command") - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then no error should be returned and command should be executed - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if !execCalled { - t.Error("Expected shell.Exec to be called") - } - if capturedCommand != "test-command" { - t.Errorf("Expected command 'test-command', got '%s'", capturedCommand) - } - if len(capturedArgs) != 0 { - t.Errorf("Expected no args, got %v", capturedArgs) - } - }) - - t.Run("ReturnsErrorWhenNoCommandProvided", func(t *testing.T) { - // Given an exec pipeline with no command in context - pipeline, _ := setup(t) - - ctx := context.Background() - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "no command provided in context" { - t.Errorf("Expected 'no command provided in context', got: %v", err) - } - }) - - t.Run("ReturnsErrorWhenCommandIsEmpty", func(t *testing.T) { - // Given an exec pipeline with empty command - pipeline, _ := setup(t) - - ctx := context.WithValue(context.Background(), "command", "") - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "no command provided in context" { - t.Errorf("Expected 'no command provided in context', got: %v", err) - } - }) - - t.Run("ReturnsErrorWhenCommandIsNotString", func(t *testing.T) { - // Given an exec pipeline with non-string command - pipeline, _ := setup(t) - - ctx := context.WithValue(context.Background(), "command", 123) - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "no command provided in context" { - t.Errorf("Expected 'no command provided in context', got: %v", err) - } - }) - - t.Run("ReturnsErrorWhenShellExecFails", func(t *testing.T) { - // Given an exec pipeline with failing shell exec - pipeline, mocks := setup(t) - - mocks.Shell.ExecFunc = func(command string, args ...string) (string, error) { - return "", fmt.Errorf("exec failed") - } - - ctx := context.WithValue(context.Background(), "command", "test-command") - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "command execution failed: exec failed" { - t.Errorf("Expected 'command execution failed: exec failed', got: %v", err) - } - }) - - t.Run("HandlesArgsAsNonSliceType", func(t *testing.T) { - // Given an exec pipeline with args as non-slice type - pipeline, _ := setup(t) - - ctx := context.WithValue(context.Background(), "command", "test-command") - ctx = context.WithValue(ctx, "args", "not-a-slice") - - // When executing the pipeline - // Then it should panic due to invalid type assertion - defer func() { - if r := recover(); r == nil { - t.Error("Expected panic due to invalid type assertion") - } - }() - - pipeline.Execute(ctx) - }) -} diff --git a/pkg/pipelines/init.go b/pkg/pipelines/init.go deleted file mode 100644 index ddd745fc5..000000000 --- a/pkg/pipelines/init.go +++ /dev/null @@ -1,496 +0,0 @@ -package pipelines - -import ( - "context" - "fmt" - "os" - "path/filepath" - "runtime" - "strings" - - "github.com/windsorcli/cli/pkg/composer/artifact" - "github.com/windsorcli/cli/pkg/composer/blueprint" - "github.com/windsorcli/cli/pkg/composer/terraform" - "github.com/windsorcli/cli/pkg/constants" - "github.com/windsorcli/cli/pkg/context/config" - 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/generators" - terraforminfra "github.com/windsorcli/cli/pkg/provisioner/terraform" - "github.com/windsorcli/cli/pkg/workstation/network" - "github.com/windsorcli/cli/pkg/workstation/services" - "github.com/windsorcli/cli/pkg/workstation/virt" -) - -// The InitPipeline is a specialized component that manages application initialization functionality. -// It provides init-specific command execution including configuration setup, context management, -// flag processing, and component initialization for the Windsor CLI init command. -// The InitPipeline handles the complete initialization workflow including default configuration -// setting, blueprint processing, and infrastructure component setup. - -// ============================================================================= -// Types -// ============================================================================= - -// InitPipeline handles the initialization of a Windsor project -type InitPipeline struct { - BasePipeline - blueprintHandler blueprint.BlueprintHandler - toolsManager tools.ToolsManager - stack terraforminfra.Stack - generators []generators.Generator - artifactBuilder artifact.Artifact - services []services.Service - virtualMachine virt.VirtualMachine - containerRuntime virt.ContainerRuntime - networkManager network.NetworkManager - terraformResolvers []terraform.ModuleResolver - fallbackBlueprintURL string -} - -// ============================================================================= -// Constructor -// ============================================================================= - -// NewInitPipeline creates a new InitPipeline instance -func NewInitPipeline() *InitPipeline { - return &InitPipeline{ - BasePipeline: *NewBasePipeline(), - } -} - -// ============================================================================= -// Public Methods -// ============================================================================= - -// Initialize sets up the init pipeline components including dependency injection container, -// configuration handler, blueprint handler, tools manager, stack, generators, bundlers, -// services, virtual machine components, terraform resolvers, and template renderer. -// It applies default configuration early so that service creation can access correct configuration values. -func (p *InitPipeline) Initialize(injector di.Injector, ctx context.Context) error { - if err := p.BasePipeline.Initialize(injector, ctx); err != nil { - return err - } - - // Configuration phase - - contextName := p.determineContextName(ctx) - if err := p.configHandler.SetContext(contextName); err != nil { - return fmt.Errorf("Error setting context value: %w", err) - } - - isLoaded := p.configHandler.IsLoaded() - if !isLoaded { - if err := p.setDefaultConfiguration(ctx, contextName); err != nil { - return err - } - } - - if err := p.processPlatformConfiguration(ctx); err != nil { - return err - } - - if err := p.configHandler.GenerateContextID(); err != nil { - return fmt.Errorf("failed to generate context ID: %w", err) - } - - if err := p.configHandler.SaveConfig(); err != nil { - return fmt.Errorf("Error saving config file: %w", err) - } - - // Reload config to ensure everything is synchronized in memory - if err := p.configHandler.LoadConfig(); err != nil { - return fmt.Errorf("failed to reload context config: %w", err) - } - - // Component Collection Phase - - kubernetesClient := p.withKubernetesClient() - if kubernetesClient != nil { - p.injector.Register("kubernetesClient", kubernetesClient) - } - - kubernetesManager := p.withKubernetesManager() - - p.blueprintHandler = p.withBlueprintHandler() - p.toolsManager = p.withToolsManager() - - if p.injector.Resolve("terraformEnv") == nil { - terraformEnv := envvars.NewTerraformEnvPrinter(p.injector) - p.injector.Register("terraformEnv", terraformEnv) - } - - p.stack = p.withStack() - p.artifactBuilder = p.withArtifactBuilder() - - generators, err := p.withGenerators() - if err != nil { - return fmt.Errorf("failed to create generators: %w", err) - } - p.generators = generators - - services, err := p.withServices() - if err != nil { - return fmt.Errorf("failed to create services: %w", err) - } - p.services = services - - terraformResolvers, err := p.withTerraformResolvers() - if err != nil { - return fmt.Errorf("failed to create terraform resolvers: %w", err) - } - p.terraformResolvers = terraformResolvers - - p.networkManager = p.withNetworking() - p.virtualMachine = p.withVirtualMachine() - p.containerRuntime = p.withContainerRuntime() - - // Component Initialization Phase - - if kubernetesManager != nil { - if err := kubernetesManager.Initialize(); err != nil { - return fmt.Errorf("failed to initialize kubernetes manager: %w", err) - } - } - - if p.blueprintHandler != nil { - if err := p.blueprintHandler.Initialize(); err != nil { - return fmt.Errorf("failed to initialize blueprint handler: %w", err) - } - } - - if p.toolsManager != nil { - if err := p.toolsManager.Initialize(); err != nil { - return fmt.Errorf("failed to initialize tools manager: %w", err) - } - } - - if p.stack != nil { - if err := p.stack.Initialize(); err != nil { - return fmt.Errorf("failed to initialize stack: %w", err) - } - } - - if p.artifactBuilder != nil { - if err := p.artifactBuilder.Initialize(p.injector); err != nil { - return fmt.Errorf("failed to initialize artifact builder: %w", err) - } - } - - for _, generator := range p.generators { - if err := generator.Initialize(); err != nil { - return fmt.Errorf("failed to initialize generator: %w", err) - } - } - - for _, service := range p.services { - if err := service.Initialize(); err != nil { - return fmt.Errorf("failed to initialize service: %w", err) - } - } - - for _, terraformResolver := range p.terraformResolvers { - if err := terraformResolver.Initialize(); err != nil { - return fmt.Errorf("failed to initialize terraform resolver: %w", err) - } - } - - if p.networkManager != nil { - if err := p.networkManager.Initialize(p.services); err != nil { - return fmt.Errorf("failed to initialize network manager: %w", err) - } - } - - if p.virtualMachine != nil { - if err := p.virtualMachine.Initialize(); err != nil { - return fmt.Errorf("failed to initialize virtual machine: %w", err) - } - } - - if p.containerRuntime != nil { - if err := p.containerRuntime.Initialize(); err != nil { - return fmt.Errorf("failed to initialize container runtime: %w", err) - } - } - - if secureShell := p.injector.Resolve("secureShell"); secureShell != nil { - if secureShellInterface, ok := secureShell.(shell.Shell); ok { - if err := secureShellInterface.Initialize(); err != nil { - return fmt.Errorf("failed to initialize secure shell: %w", err) - } - } - } - - return nil -} - -// Execute performs initialization by writing reset tokens, processing templates, handling blueprints separately, -// writing blueprint files, resolving Terraform modules, and generating final output files. -func (p *InitPipeline) Execute(ctx context.Context) error { - - // Phase 1: Setup - if _, err := p.shell.WriteResetToken(); err != nil { - return fmt.Errorf("Error writing reset token: %w", err) - } - - // Phase 2: Blueprint loading - if ctx.Value("blueprint") == nil && p.artifactBuilder != nil { - hasLocalTemplates := p.hasLocalTemplates() - if !hasLocalTemplates { - p.fallbackBlueprintURL = constants.GetEffectiveBlueprintURL() - } - } - - // Phase 3: Blueprint handling - reset := false - if resetValue := ctx.Value("reset"); resetValue != nil { - reset = resetValue.(bool) - } - if err := p.handleBlueprintLoading(ctx, reset); err != nil { - return err - } - if err := p.blueprintHandler.Write(reset); err != nil { - return fmt.Errorf("failed to write blueprint file: %w", err) - } - - // Phase 4: Terraform module resolution - for _, resolver := range p.terraformResolvers { - if err := resolver.ProcessModules(); err != nil { - return fmt.Errorf("failed to process terraform modules: %w", err) - } - } - - // Phase 5: Final file generation - for _, generator := range p.generators { - if err := generator.Generate(map[string]any{}, reset); err != nil { - return fmt.Errorf("failed to generate from template data: %w", err) - } - } - - if err := p.writeConfigurationFiles(); err != nil { - return err - } - - hasSetFlags := false - if setFlagsValue := ctx.Value("hasSetFlags"); setFlagsValue != nil { - hasSetFlags = setFlagsValue.(bool) - } - - if err := p.configHandler.SaveConfig(hasSetFlags); err != nil { - return fmt.Errorf("failed to save configuration: %w", err) - } - - fmt.Fprintln(os.Stderr, "Initialization successful") - - return nil -} - -// ============================================================================= -// Private Methods -// ============================================================================= - -// setDefaultConfiguration sets default config values based on provider and VM driver detection. -// For local providers, uses config.DefaultConfig_Localhost if VM driver is "docker-desktop", -// else uses config.DefaultConfig_Full. For non-local, uses config.DefaultConfig. -// On darwin/windows, sets "vm.driver" to "docker-desktop"; otherwise to "docker". -// If provider is unset and context is local, sets provider to "local". -// Returns error if any config operation fails. -func (p *InitPipeline) setDefaultConfiguration(_ context.Context, contextName string) error { - existingProvider := p.configHandler.GetString("provider") - - var isLocalContext bool - if existingProvider != "" { - // Treat "generic" provider with "local" context name as local context - isLocalContext = existingProvider == "generic" && (contextName == "local" || strings.HasPrefix(contextName, "local-")) - } else { - isLocalContext = contextName == "local" || strings.HasPrefix(contextName, "local-") - } - - vmDriver := p.configHandler.GetString("vm.driver") - - if isLocalContext && vmDriver == "" { - switch runtime.GOOS { - case "darwin", "windows": - vmDriver = "docker-desktop" - default: - vmDriver = "docker" - } - } - - if vmDriver == "docker-desktop" { - if err := p.configHandler.SetDefault(config.DefaultConfig_Localhost); err != nil { - return fmt.Errorf("Error setting default config: %w", err) - } - } else if isLocalContext { - if err := p.configHandler.SetDefault(config.DefaultConfig_Full); err != nil { - return fmt.Errorf("Error setting default config: %w", err) - } - } else { - if err := p.configHandler.SetDefault(config.DefaultConfig); err != nil { - return fmt.Errorf("Error setting default config: %w", err) - } - } - - if isLocalContext && p.configHandler.GetString("vm.driver") == "" && vmDriver != "" { - if err := p.configHandler.Set("vm.driver", vmDriver); err != nil { - return fmt.Errorf("Error setting vm.driver: %w", err) - } - } - - if existingProvider == "" { - if contextName == "local" || strings.HasPrefix(contextName, "local-") { - if err := p.configHandler.Set("provider", "generic"); err != nil { - return fmt.Errorf("Error setting provider from context name: %w", err) - } - } - } - - return nil -} - -// processPlatformConfiguration applies provider-specific configuration settings based on the "provider" value in the configuration handler. -// Since defaults are already applied in setDefaultConfiguration, this function only sets provider-specific overrides. -// 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". -// Returns an error if any configuration operation fails. -func (p *InitPipeline) processPlatformConfiguration(_ context.Context) error { - provider := p.configHandler.GetString("provider") - if provider == "" { - return nil - } - - switch provider { - case "aws": - if err := p.configHandler.Set("aws.enabled", true); err != nil { - return fmt.Errorf("Error setting aws.enabled: %w", err) - } - if err := p.configHandler.Set("cluster.driver", "eks"); err != nil { - return fmt.Errorf("Error setting cluster.driver: %w", err) - } - case "azure": - if err := p.configHandler.Set("azure.enabled", true); err != nil { - return fmt.Errorf("Error setting azure.enabled: %w", err) - } - if err := p.configHandler.Set("cluster.driver", "aks"); err != nil { - return fmt.Errorf("Error setting cluster.driver: %w", err) - } - case "generic": - if err := p.configHandler.Set("cluster.driver", "talos"); err != nil { - return fmt.Errorf("Error setting cluster.driver: %w", err) - } - } - - return nil -} - -// writeConfigurationFiles writes configuration files for all managed components in the InitPipeline. -// It sequentially invokes WriteManifest or WriteConfig on the tools manager, each registered service, -// the virtual machine, and the container runtime if present. Returns an error if any write operation fails. -func (p *InitPipeline) writeConfigurationFiles() error { - if p.toolsManager != nil { - if err := p.toolsManager.WriteManifest(); err != nil { - return fmt.Errorf("error writing tools manifest: %w", err) - } - } - - for _, service := range p.services { - if err := service.WriteConfig(); err != nil { - return fmt.Errorf("error writing service config: %w", err) - } - } - - if p.virtualMachine != nil { - if err := p.virtualMachine.WriteConfig(); err != nil { - return fmt.Errorf("error writing virtual machine config: %w", err) - } - } - - if p.containerRuntime != nil { - if err := p.containerRuntime.WriteConfig(); err != nil { - return fmt.Errorf("error writing container runtime config: %w", err) - } - } - - return nil -} - -// handleBlueprintLoading loads blueprint data for the InitPipeline based on the reset flag and blueprint file presence. -// If reset is true, loads blueprint from template data if available. If reset is false, prefers an existing blueprint.yaml file over template data. -// If no template blueprint data exists, loads from existing config. Returns an error if loading fails. -func (p *InitPipeline) handleBlueprintLoading(ctx context.Context, reset bool) error { - shouldLoadFromTemplate := false - usingLocalTemplates := p.hasLocalTemplates() - - if reset { - shouldLoadFromTemplate = true - } else { - configRoot, err := p.configHandler.GetConfigRoot() - if err != nil { - return fmt.Errorf("error getting config root: %w", err) - } - blueprintPath := filepath.Join(configRoot, "blueprint.yaml") - if _, err := p.shims.Stat(blueprintPath); err != nil { - shouldLoadFromTemplate = true - } - } - - if shouldLoadFromTemplate { - if p.fallbackBlueprintURL != "" { - ctx = context.WithValue(ctx, "blueprint", p.fallbackBlueprintURL) - } - - _, err := p.blueprintHandler.GetLocalTemplateData() - if err != nil { - return fmt.Errorf("failed to get template data: %w", err) - } - } else { - if err := p.blueprintHandler.LoadConfig(); err != nil { - return fmt.Errorf("failed to load blueprint config: %w", err) - } - } - - if !usingLocalTemplates { - sources := p.blueprintHandler.GetSources() - if len(sources) > 0 && p.artifactBuilder != nil { - var ociURLs []string - for _, source := range sources { - if strings.HasPrefix(source.Url, "oci://") { - ociURLs = append(ociURLs, source.Url) - } - } - if len(ociURLs) > 0 { - _, err := p.artifactBuilder.Pull(ociURLs) - if err != nil { - return fmt.Errorf("failed to load OCI sources: %w", err) - } - } - } - } - - return nil -} - -// hasLocalTemplates checks if the contexts/_template directory exists in the project. -func (p *InitPipeline) hasLocalTemplates() bool { - if p.shell == nil || p.shims == nil { - return false - } - - projectRoot, err := p.shell.GetProjectRoot() - if err != nil { - return false - } - - templateDir := filepath.Join(projectRoot, "contexts", "_template") - _, err = p.shims.Stat(templateDir) - return err == nil -} - -// ============================================================================= -// Interface Compliance -// ============================================================================= - -var _ Pipeline = (*InitPipeline)(nil) diff --git a/pkg/pipelines/init_test.go b/pkg/pipelines/init_test.go deleted file mode 100644 index 2692ec8bd..000000000 --- a/pkg/pipelines/init_test.go +++ /dev/null @@ -1,1255 +0,0 @@ -package pipelines - -import ( - "context" - "fmt" - "os" - "strings" - "testing" - "time" - - "github.com/windsorcli/cli/api/v1alpha1" - "github.com/windsorcli/cli/pkg/context/config" - "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/context/tools" - "github.com/windsorcli/cli/pkg/provisioner/kubernetes" - terraforminfra "github.com/windsorcli/cli/pkg/provisioner/terraform" - "github.com/windsorcli/cli/pkg/composer/artifact" - "github.com/windsorcli/cli/pkg/composer/blueprint" - "github.com/windsorcli/cli/pkg/context/shell" - "github.com/windsorcli/cli/pkg/workstation/virt" -) - -// ============================================================================= -// Test Setup -// ============================================================================= - -// patchMockFileInfo implements os.FileInfo for testing -type patchMockFileInfo struct { - isDir bool -} - -func (m *patchMockFileInfo) Name() string { return "mock" } -func (m *patchMockFileInfo) Size() int64 { return 0 } -func (m *patchMockFileInfo) Mode() os.FileMode { return 0 } -func (m *patchMockFileInfo) ModTime() time.Time { return time.Time{} } -func (m *patchMockFileInfo) IsDir() bool { return m.isDir } -func (m *patchMockFileInfo) Sys() interface{} { return nil } - -type InitMocks struct { - *Mocks - BlueprintHandler *blueprint.MockBlueprintHandler - KubernetesManager *kubernetes.MockKubernetesManager - ToolsManager *tools.MockToolsManager - Stack *terraforminfra.MockStack - VirtualMachine *virt.MockVirt - ContainerRuntime *virt.MockVirt - ArtifactBuilder *artifact.MockArtifact -} - -func setupInitMocks(t *testing.T, opts ...*SetupOptions) *InitMocks { - t.Helper() - - // Create setup options, preserving any provided options - setupOptions := &SetupOptions{} - if len(opts) > 0 && opts[0] != nil { - setupOptions = opts[0] - } - - baseMocks := setupMocks(t, setupOptions) - - // Initialize the config handler if it's a real one - if setupOptions.ConfigHandler == nil { - configHandler := baseMocks.ConfigHandler - configHandler.SetContext("mock-context") - - // Load base config - configYAML := ` -apiVersion: v1alpha1 -contexts: - mock-context: - dns: - domain: mock.domain.com - network: - cidr_block: 10.0.0.0/24` - - if err := configHandler.LoadConfigString(configYAML); err != nil { - t.Fatalf("Failed to load config: %v", err) - } - } - - // Add init-specific shell mock behaviors - baseMocks.Shell.WriteResetTokenFunc = func() (string, error) { return "mock-token", nil } - baseMocks.Shell.AddCurrentDirToTrustedFileFunc = func() error { return nil } - - // Setup blueprint handler mock - mockBlueprintHandler := blueprint.NewMockBlueprintHandler(baseMocks.Injector) - mockBlueprintHandler.InitializeFunc = func() error { return nil } - mockBlueprintHandler.LoadConfigFunc = func() error { return nil } - mockBlueprintHandler.GetDefaultTemplateDataFunc = func(contextName string) (map[string][]byte, error) { - return make(map[string][]byte), nil - } - baseMocks.Injector.Register("blueprintHandler", mockBlueprintHandler) - - // Setup kubernetes manager mock - mockKubernetesManager := kubernetes.NewMockKubernetesManager(nil) - mockKubernetesManager.InitializeFunc = func() error { return nil } - baseMocks.Injector.Register("kubernetesManager", mockKubernetesManager) - - // Setup tools manager mock - mockToolsManager := tools.NewMockToolsManager() - mockToolsManager.InitializeFunc = func() error { return nil } - mockToolsManager.WriteManifestFunc = func() error { return nil } - baseMocks.Injector.Register("toolsManager", mockToolsManager) - - // Setup stack mock - mockStack := terraforminfra.NewMockStack(baseMocks.Injector) - mockStack.InitializeFunc = func() error { return nil } - baseMocks.Injector.Register("stack", mockStack) - - // Setup virtual machine mock - mockVirtualMachine := virt.NewMockVirt() - mockVirtualMachine.WriteConfigFunc = func() error { return nil } - baseMocks.Injector.Register("virtualMachine", mockVirtualMachine) - - // Setup container runtime mock - mockContainerRuntime := virt.NewMockVirt() - mockContainerRuntime.WriteConfigFunc = func() error { return nil } - baseMocks.Injector.Register("containerRuntime", mockContainerRuntime) - - // Setup artifact builder mock - mockArtifactBuilder := artifact.NewMockArtifact() - mockArtifactBuilder.InitializeFunc = func(injector di.Injector) error { return nil } - baseMocks.Injector.Register("artifactBuilder", mockArtifactBuilder) - - return &InitMocks{ - Mocks: baseMocks, - BlueprintHandler: mockBlueprintHandler, - KubernetesManager: mockKubernetesManager, - ToolsManager: mockToolsManager, - Stack: mockStack, - VirtualMachine: mockVirtualMachine, - ContainerRuntime: mockContainerRuntime, - ArtifactBuilder: mockArtifactBuilder, - } -} - -// ============================================================================= -// Test Constructor -// ============================================================================= - -func TestNewInitPipeline(t *testing.T) { - t.Run("CreatesWithDefaults", func(t *testing.T) { - // Given creating a new init pipeline - pipeline := NewInitPipeline() - - // Then pipeline should not be nil - if pipeline == nil { - t.Fatal("Expected pipeline to not be nil") - } - }) -} - -// ============================================================================= -// Test Public Methods -// ============================================================================= - -func TestInitPipeline_Initialize(t *testing.T) { - setup := func(t *testing.T, opts ...*SetupOptions) (*InitPipeline, *InitMocks) { - t.Helper() - pipeline := NewInitPipeline() - mocks := setupInitMocks(t, opts...) - return pipeline, mocks - } - - t.Run("InitializesSuccessfully", func(t *testing.T) { - // Given an init pipeline with mock dependencies - pipeline, mocks := setup(t) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - // Test initialization failures - initFailureTests := []struct { - name string - setupMock func(*InitMocks) - expectedErr string - }{ - { - name: "ReturnsErrorWhenShellInitializeFails", - setupMock: func(mocks *InitMocks) { - mockShell := shell.NewMockShell() - mockShell.InitializeFunc = func() error { - return fmt.Errorf("shell initialization failed") - } - mocks.Injector.Register("shell", mockShell) - }, - expectedErr: "failed to initialize shell: shell initialization failed", - }, - { - name: "ReturnsErrorWhenBlueprintHandlerInitializeFails", - setupMock: func(mocks *InitMocks) { - mocks.BlueprintHandler.InitializeFunc = func() error { - return fmt.Errorf("blueprint handler failed") - } - }, - expectedErr: "failed to initialize blueprint handler: blueprint handler failed", - }, - { - name: "ReturnsErrorWhenKubernetesManagerInitializeFails", - setupMock: func(mocks *InitMocks) { - mocks.KubernetesManager.InitializeFunc = func() error { - return fmt.Errorf("kubernetes manager failed") - } - }, - expectedErr: "failed to initialize kubernetes manager: kubernetes manager failed", - }, - { - name: "ReturnsErrorWhenToolsManagerInitializeFails", - setupMock: func(mocks *InitMocks) { - mocks.ToolsManager.InitializeFunc = func() error { - return fmt.Errorf("tools manager failed") - } - }, - expectedErr: "failed to initialize tools manager: tools manager failed", - }, - { - name: "ReturnsErrorWhenStackInitializeFails", - setupMock: func(mocks *InitMocks) { - mocks.Stack.InitializeFunc = func() error { - return fmt.Errorf("stack failed") - } - }, - expectedErr: "failed to initialize stack: stack failed", - }, - { - name: "ReturnsErrorWhenArtifactBuilderInitializeFails", - setupMock: func(mocks *InitMocks) { - mocks.ArtifactBuilder.InitializeFunc = func(injector di.Injector) error { - return fmt.Errorf("artifact builder failed") - } - }, - expectedErr: "failed to initialize artifact builder: artifact builder failed", - }, - } - - for _, tt := range initFailureTests { - t.Run(tt.name, func(t *testing.T) { - // Given an init pipeline with failing component - pipeline, mocks := setup(t) - tt.setupMock(mocks) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != tt.expectedErr { - t.Errorf("Expected error %q, got %q", tt.expectedErr, err.Error()) - } - }) - } - - t.Run("InitializesSecureShellWhenRegistered", func(t *testing.T) { - // Given an init pipeline with secure shell registered - pipeline, mocks := setup(t) - - // Create mock secure shell - mockSecureShell := shell.NewMockShell() - secureShellInitialized := false - mockSecureShell.InitializeFunc = func() error { - secureShellInitialized = true - return nil - } - mocks.Injector.Register("secureShell", mockSecureShell) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And secure shell should be initialized - if !secureShellInitialized { - t.Error("Expected secure shell to be initialized") - } - }) - - t.Run("ReturnsErrorWhenSecureShellInitializeFails", func(t *testing.T) { - // Given an init pipeline with failing secure shell - pipeline, mocks := setup(t) - - // Create mock secure shell that fails to initialize - mockSecureShell := shell.NewMockShell() - mockSecureShell.InitializeFunc = func() error { - return fmt.Errorf("secure shell failed") - } - mocks.Injector.Register("secureShell", mockSecureShell) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "failed to initialize secure shell: secure shell failed" { - t.Errorf("Expected secure shell error, got %q", err.Error()) - } - }) - - t.Run("SkipsSecureShellWhenNotRegistered", func(t *testing.T) { - // Given an init pipeline without secure shell registered - pipeline, mocks := setup(t) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("SkipsSecureShellWhenRegisteredTypeIsIncorrect", func(t *testing.T) { - // Given an init pipeline with incorrectly typed secure shell - pipeline, mocks := setup(t) - - // Register something that's not a shell.Shell - mocks.Injector.Register("secureShell", "not-a-shell") - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) -} - -func TestInitPipeline_Execute(t *testing.T) { - // Given a pipeline with mocks - mocks := setupInitMocks(t) - pipeline := NewInitPipeline() - pipeline.blueprintHandler = mocks.BlueprintHandler - pipeline.shell = mocks.Shell - pipeline.toolsManager = mocks.ToolsManager - pipeline.configHandler = mocks.ConfigHandler - - t.Run("ExecutesSuccessfully", func(t *testing.T) { - // Given successful mocks - mocks.Shell.WriteResetTokenFunc = func() (string, error) { - return "token", nil - } - mocks.BlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { - return map[string][]byte{"test.jsonnet": []byte("test")}, nil - } - mocks.BlueprintHandler.WriteFunc = func(overwrite ...bool) error { - return nil - } - mocks.BlueprintHandler.LoadConfigFunc = func() error { - return nil - } - - // Initialize the pipeline properly - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When executing the pipeline - err := pipeline.Execute(context.Background()) - - // Then no error should occur - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("ReturnsErrorWhenWriteResetTokenFails", func(t *testing.T) { - // Given shell write reset token fails - mocks.Shell.WriteResetTokenFunc = func() (string, error) { - return "", fmt.Errorf("reset token error") - } - - // Initialize the pipeline properly - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When executing the pipeline - err := pipeline.Execute(context.Background()) - - // Then error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "Error writing reset token") { - t.Errorf("Expected reset token error, got %v", err) - } - }) - - t.Run("ReturnsErrorWhenBlueprintLoadConfigFails", func(t *testing.T) { - // Given successful reset token - mocks.Shell.WriteResetTokenFunc = func() (string, error) { - return "token", nil - } - // And blueprint handler returns error on GetLocalTemplateData (template loading path) - mocks.BlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { - return nil, fmt.Errorf("template data error") - } - - // Initialize the pipeline properly - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When executing the pipeline - err := pipeline.Execute(context.Background()) - - // Then error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to get template data") { - t.Errorf("Expected template data error, got %v", err) - } - }) - - t.Run("ReturnsErrorWhenBlueprintWriteFails", func(t *testing.T) { - // Given successful reset token - mocks.Shell.WriteResetTokenFunc = func() (string, error) { - return "token", nil - } - // And successful template data loading - mocks.BlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { - return map[string][]byte{"blueprint": []byte("test")}, nil - } - // And blueprint write fails - mocks.BlueprintHandler.WriteFunc = func(overwrite ...bool) error { - return fmt.Errorf("blueprint write error") - } - - // Initialize the pipeline properly - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When executing the pipeline - err := pipeline.Execute(context.Background()) - - // Then error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to write blueprint file") { - t.Errorf("Expected blueprint write error, got %v", err) - } - }) -} - -// ============================================================================= -// Test Private Methods -// ============================================================================= - -func TestInitPipeline_setDefaultConfiguration(t *testing.T) { - setup := func(t *testing.T, vmDriver, platform string) (*InitPipeline, *config.MockConfigHandler) { - t.Helper() - pipeline := &InitPipeline{} - - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - switch key { - case "vm.driver": - return vmDriver - case "provider": - return platform - default: - return "" - } - } - mockConfigHandler.SetDefaultFunc = func(defaultConfig v1alpha1.Context) error { - return nil - } - mockConfigHandler.SetFunc = func(key string, value interface{}) error { - return nil - } - pipeline.configHandler = mockConfigHandler - - return pipeline, mockConfigHandler - } - - configurationTests := []struct { - name string - vmDriver string - contextName string - expectError bool - }{ - {name: "HandlesDockerDesktopDriver", vmDriver: "docker-desktop", contextName: "test"}, - {name: "HandlesColimaDriver", vmDriver: "colima", contextName: "test"}, - {name: "HandlesDockerDriver", vmDriver: "docker", contextName: "test"}, - {name: "HandlesLocalContextWithoutDriver", vmDriver: "", contextName: "local"}, - {name: "HandlesLocalPrefixContextWithoutDriver", vmDriver: "", contextName: "local-dev"}, - {name: "HandlesUnknownDriver", vmDriver: "unknown", contextName: "test"}, - } - - for _, tt := range configurationTests { - t.Run(tt.name, func(t *testing.T) { - // Given a pipeline with specific configuration - pipeline, _ := setup(t, tt.vmDriver, "") - - // When setDefaultConfiguration is called - err := pipeline.setDefaultConfiguration(context.Background(), tt.contextName) - - // Then should complete successfully - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - } - - t.Run("UsesContextNameAsProviderWhenNoProviderSet", func(t *testing.T) { - // Given a pipeline with no provider set and "local" context name - pipeline, mockConfigHandler := setup(t, "", "") - providerSet := false - mockConfigHandler.SetFunc = func(key string, value interface{}) error { - if key == "provider" { - providerSet = true - } - return nil - } - - // When setDefaultConfiguration is called with "local" context - err := pipeline.setDefaultConfiguration(context.Background(), "local") - - // Then should set provider to "generic" and complete successfully - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if !providerSet { - t.Error("Expected provider to be set from context name") - } - }) - - t.Run("DoesNotSetProviderForNonLocalContexts", func(t *testing.T) { - // Given a pipeline with no provider set and "aws" context name - pipeline, mockConfigHandler := setup(t, "", "") - providerSet := false - mockConfigHandler.SetFunc = func(key string, value interface{}) error { - if key == "provider" { - providerSet = true - } - return nil - } - - // When setDefaultConfiguration is called with "aws" context - err := pipeline.setDefaultConfiguration(context.Background(), "aws") - - // Then should not set provider automatically - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if providerSet { - t.Error("Expected provider not to be set automatically for non-local contexts") - } - }) - - t.Run("PrioritizesExplicitProviderOverContextName", func(t *testing.T) { - // Given a pipeline with explicit provider "aws" and "local" context name - pipeline, mockConfigHandler := setup(t, "", "aws") - var appliedDefaults v1alpha1.Context - mockConfigHandler.SetDefaultFunc = func(defaultConfig v1alpha1.Context) error { - appliedDefaults = defaultConfig - return nil - } - - // When setDefaultConfiguration is called with "local" context - err := pipeline.setDefaultConfiguration(context.Background(), "local") - - // Then should complete successfully and apply minimal defaults (not local defaults) - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // Should apply DefaultConfig (minimal) instead of DefaultConfig_Localhost (full local) - // We can't directly compare the structs, but we can check that it's not the localhost config - // by verifying that local-specific fields are not present - if appliedDefaults.Docker != nil { - t.Error("Expected no docker config when using explicit AWS provider") - } - if appliedDefaults.Git != nil { - t.Error("Expected no git config when using explicit AWS provider") - } - }) - - t.Run("UsesContextNameWhenNoProviderSpecified", func(t *testing.T) { - // Given a pipeline with no provider set and "local" context name - pipeline, mockConfigHandler := setup(t, "", "") - var appliedDefaults v1alpha1.Context - mockConfigHandler.SetDefaultFunc = func(defaultConfig v1alpha1.Context) error { - appliedDefaults = defaultConfig - return nil - } - - // When setDefaultConfiguration is called with "local" context - err := pipeline.setDefaultConfiguration(context.Background(), "local") - - // Then should complete successfully and apply local defaults - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // Should apply local defaults (DefaultConfig_Localhost) which includes docker and git - if appliedDefaults.Docker == nil { - t.Error("Expected docker config when using local context name") - } - if appliedDefaults.Git == nil { - t.Error("Expected git config when using local context name") - } - }) - - t.Run("UsesContextNameAsProviderForLocal", func(t *testing.T) { - // Given a pipeline with no provider set and "local" context name - pipeline, mockConfigHandler := setup(t, "", "") - var setProvider string - mockConfigHandler.SetFunc = func(key string, value interface{}) error { - if key == "provider" { - setProvider = value.(string) - } - return nil - } - - // When setDefaultConfiguration is called with "local" context - err := pipeline.setDefaultConfiguration(context.Background(), "local") - - // Then should set provider to "generic" and complete successfully - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if setProvider != "generic" { - t.Errorf("Expected provider to be set to 'generic', got %q", setProvider) - } - }) - - t.Run("DoesNotUseContextNameAsProviderWhenProviderAlreadySet", func(t *testing.T) { - // Given a pipeline with provider already set to "aws" - pipeline, mockConfigHandler := setup(t, "", "aws") - providerSetCount := 0 - mockConfigHandler.SetFunc = func(key string, value interface{}) error { - if key == "provider" { - providerSetCount++ - } - return nil - } - - // When setDefaultConfiguration is called with "azure" context - err := pipeline.setDefaultConfiguration(context.Background(), "azure") - - // Then should not set provider again and complete successfully - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if providerSetCount > 0 { - t.Errorf("Expected provider not to be set again, but it was set %d times", providerSetCount) - } - }) - - t.Run("DoesNotUseContextNameAsProviderForUnknownProvider", func(t *testing.T) { - // Given a pipeline with no provider set and unknown context name - pipeline, mockConfigHandler := setup(t, "", "") - providerSet := false - mockConfigHandler.SetFunc = func(key string, value interface{}) error { - if key == "provider" { - providerSet = true - } - return nil - } - - // When setDefaultConfiguration is called with "unknown" context - err := pipeline.setDefaultConfiguration(context.Background(), "unknown") - - // Then should not set provider and complete successfully - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if providerSet { - t.Error("Expected provider not to be set for unknown context name") - } - }) - - t.Run("AlwaysAppliesDefaultConfigThenOverridesWithProviderSpecificSettings", func(t *testing.T) { - // Given a pipeline with provider already set - pipeline, mockConfigHandler := setup(t, "docker-desktop", "aws") - defaultConfigSet := false - mockConfigHandler.SetDefaultFunc = func(defaultConfig v1alpha1.Context) error { - defaultConfigSet = true - return nil - } - - // When setDefaultConfiguration is called - err := pipeline.setDefaultConfiguration(context.Background(), "test") - - // Then should always set default config first, then override with provider-specific settings - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if !defaultConfigSet { - t.Error("Expected default config to be set even when provider is already set") - } - }) - - t.Run("ReturnsErrorWhenSetProviderFromContextNameFails", func(t *testing.T) { - // Given a pipeline with config handler that fails on Set for provider - pipeline, mockConfigHandler := setup(t, "", "") - mockConfigHandler.SetFunc = func(key string, value interface{}) error { - if key == "provider" { - return fmt.Errorf("set provider failed") - } - return nil - } - - // When setDefaultConfiguration is called with "local" context - err := pipeline.setDefaultConfiguration(context.Background(), "local") - - // Then should return error - if err == nil { - t.Fatal("Expected error, got nil") - } - if !strings.Contains(err.Error(), "Error setting provider from context name") { - t.Errorf("Expected error to contain 'Error setting provider from context name', got %v", err) - } - }) - - t.Run("ReturnsErrorWhenSetDefaultFails", func(t *testing.T) { - // Given a pipeline with config handler that fails on SetDefault - pipeline, mockConfigHandler := setup(t, "docker-desktop", "") - mockConfigHandler.SetDefaultFunc = func(defaultConfig v1alpha1.Context) error { - return fmt.Errorf("set default failed") - } - - // When setDefaultConfiguration is called - err := pipeline.setDefaultConfiguration(context.Background(), "test") - - // Then should return error - if err == nil { - t.Fatal("Expected error, got nil") - } - if !strings.Contains(err.Error(), "Error setting default config") { - t.Errorf("Expected error to contain 'Error setting default config', got %v", err) - } - }) - - t.Run("ReturnsErrorWhenSetFails", func(t *testing.T) { - // Given a pipeline with config handler that fails on Set - pipeline, mockConfigHandler := setup(t, "", "") - mockConfigHandler.SetFunc = func(key string, value interface{}) error { - return fmt.Errorf("set context value failed") - } - - // When setDefaultConfiguration is called with "local" context - err := pipeline.setDefaultConfiguration(context.Background(), "local") - - // Then should return error - if err == nil { - t.Fatal("Expected error, got nil") - } - if !strings.Contains(err.Error(), "Error setting vm.driver") { - t.Errorf("Expected error to contain 'Error setting vm.driver', got %v", err) - } - }) -} - -func TestInitPipeline_processPlatformConfiguration(t *testing.T) { - setup := func(t *testing.T, provider string) (*InitPipeline, *config.MockConfigHandler) { - t.Helper() - pipeline := &InitPipeline{} - - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - if key == "provider" { - return provider - } - return "" - } - mockConfigHandler.SetFunc = func(key string, value interface{}) error { - return nil - } - pipeline.configHandler = mockConfigHandler - - return pipeline, mockConfigHandler - } - - providerTests := []struct { - name string - provider string - }{ - {name: "HandlesAWSProvider", provider: "aws"}, - {name: "HandlesAzureProvider", provider: "azure"}, - {name: "HandlesGenericProvider", provider: "generic"}, - {name: "HandlesEmptyProvider", provider: ""}, - } - - for _, tt := range providerTests { - t.Run(tt.name, func(t *testing.T) { - // Given a pipeline with specific provider configuration - pipeline, _ := setup(t, tt.provider) - - // When processPlatformConfiguration is called - err := pipeline.processPlatformConfiguration(context.Background()) - - // Then should complete successfully - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - } - - t.Run("ReturnsErrorWhenSetFails", func(t *testing.T) { - // Given a pipeline with platform configuration that fails - pipeline, mockConfigHandler := setup(t, "aws") - mockConfigHandler.SetFunc = func(key string, value interface{}) error { - return fmt.Errorf("config error") - } - - // When processPlatformConfiguration is called - err := pipeline.processPlatformConfiguration(context.Background()) - - // Then should return error - if err == nil { - t.Fatal("Expected error, got nil") - } - if !strings.Contains(err.Error(), "Error setting aws.enabled") { - t.Errorf("Expected error to contain 'Error setting aws.enabled', got %v", err) - } - }) -} - -func TestInitPipeline_prepareTemplateData(t *testing.T) { - t.Run("Priority1_ExplicitBlueprintOverridesLocalTemplates", func(t *testing.T) { - // Given a pipeline with both explicit blueprint and local templates - pipeline := &InitPipeline{} - - // Set up BasePipeline properly - pipeline.BasePipeline = *NewBasePipeline() - pipeline.BasePipeline.injector = di.NewInjector() - - // Mock artifact builder that succeeds - mockArtifactBuilder := artifact.NewMockArtifact() - expectedOCIData := map[string][]byte{ - "blueprint.jsonnet": []byte("{ explicit: 'oci-data' }"), - } - mockArtifactBuilder.GetTemplateDataFunc = func(ociRef string) (map[string][]byte, error) { - return expectedOCIData, nil - } - pipeline.BasePipeline.artifactBuilder = mockArtifactBuilder - - // Mock blueprint handler with local templates - mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) - mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { - return map[string][]byte{ - "blueprint.jsonnet": []byte("{ local: 'template-data' }"), - }, nil - } - pipeline.BasePipeline.injector.Register("blueprintHandler", mockBlueprintHandler) - - // Create context with explicit blueprint value - ctx := context.WithValue(context.Background(), "blueprint", "oci://registry.example.com/blueprint:latest") - - // When prepareTemplateData is called - templateData, err := pipeline.BasePipeline.prepareTemplateData(ctx) - - // Then should use explicit blueprint, not local templates - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(templateData) != 1 { - t.Errorf("Expected 1 template file, got %d", len(templateData)) - } - if string(templateData["blueprint.jsonnet"]) != "{ explicit: 'oci-data' }" { - t.Error("Expected explicit blueprint data to override local templates") - } - }) - - t.Run("Priority1_ExplicitBlueprintFailsWithError", func(t *testing.T) { - // Given a pipeline with explicit blueprint that fails - pipeline := &InitPipeline{} - - // Set up BasePipeline properly - pipeline.BasePipeline = *NewBasePipeline() - pipeline.BasePipeline.injector = di.NewInjector() - - mockArtifactBuilder := artifact.NewMockArtifact() - mockArtifactBuilder.GetTemplateDataFunc = func(ociRef string) (map[string][]byte, error) { - return nil, fmt.Errorf("OCI pull failed") - } - pipeline.BasePipeline.artifactBuilder = mockArtifactBuilder - - ctx := context.WithValue(context.Background(), "blueprint", "oci://registry.example.com/blueprint:latest") - - // When prepareTemplateData is called - templateData, err := pipeline.BasePipeline.prepareTemplateData(ctx) - - // Then should return error - if err == nil { - t.Fatal("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to get template data from blueprint") { - t.Errorf("Expected error to contain 'failed to get template data from blueprint', got %v", err) - } - if templateData != nil { - t.Error("Expected nil template data on error") - } - }) - - t.Run("Priority2_LocalTemplatesWhenNoExplicitBlueprint", func(t *testing.T) { - // Given a pipeline with local templates but no explicit blueprint - pipeline := &InitPipeline{} - - // Set up BasePipeline properly - pipeline.BasePipeline = *NewBasePipeline() - injector := di.NewInjector() - - mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) - expectedLocalData := map[string][]byte{ - "blueprint.jsonnet": []byte("{ local: 'template-data' }"), - } - mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { - return expectedLocalData, nil - } - injector.Register("blueprintHandler", mockBlueprintHandler) - - // Initialize the pipeline to set up all components - if err := pipeline.BasePipeline.Initialize(injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When prepareTemplateData is called with no blueprint context - templateData, err := pipeline.BasePipeline.prepareTemplateData(context.Background()) - - // Then should use local template data - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(templateData) != 1 { - t.Errorf("Expected 1 template file, got %d", len(templateData)) - } - if string(templateData["blueprint.jsonnet"]) != "{ local: 'template-data' }" { - t.Error("Expected local template data") - } - }) - - t.Run("Priority3_DefaultOCIURLWhenNoLocalTemplates", func(t *testing.T) { - // Given a pipeline with no local templates and artifact builder - pipeline := &InitPipeline{} - - // Set up BasePipeline properly - pipeline.BasePipeline = *NewBasePipeline() - pipeline.BasePipeline.injector = di.NewInjector() - - // Mock artifact builder for default OCI URL - mockArtifactBuilder := artifact.NewMockArtifact() - expectedDefaultOCIData := map[string][]byte{ - "blueprint.jsonnet": []byte("{ default: 'oci-data' }"), - } - var receivedOCIRef string - mockArtifactBuilder.GetTemplateDataFunc = func(ociRef string) (map[string][]byte, error) { - receivedOCIRef = ociRef - return expectedDefaultOCIData, nil - } - pipeline.BasePipeline.artifactBuilder = mockArtifactBuilder - - // Mock blueprint handler with no local templates - mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) - mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { - return make(map[string][]byte), nil // Empty local templates - } - pipeline.BasePipeline.injector.Register("blueprintHandler", mockBlueprintHandler) - - // When prepareTemplateData is called with no blueprint context - templateData, err := pipeline.BasePipeline.prepareTemplateData(context.Background()) - - // Then should use default OCI URL and set fallback URL - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(templateData) != 1 { - t.Errorf("Expected 1 template file, got %d", len(templateData)) - } - if string(templateData["blueprint.jsonnet"]) != "{ default: 'oci-data' }" { - t.Error("Expected default OCI blueprint data") - } - // Verify the correct default OCI URL was used - if !strings.Contains(receivedOCIRef, "ghcr.io/windsorcli/core") { - t.Errorf("Expected default OCI URL to be used, got %s", receivedOCIRef) - } - }) - - t.Run("Priority4_EmbeddedDefaultWhenNoArtifactBuilder", func(t *testing.T) { - // Given a pipeline with no artifact builder - pipeline := &InitPipeline{} - - // Set up BasePipeline properly - pipeline.BasePipeline = *NewBasePipeline() - injector := di.NewInjector() - - // Mock config handler (needed for determineContextName) - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetContextFunc = func() string { - return "local" - } - injector.Register("configHandler", mockConfigHandler) - - // Mock blueprint handler with no local templates but default template - mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) - mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { - return make(map[string][]byte), nil // Empty local templates - } - expectedDefaultData := map[string][]byte{ - "blueprint.jsonnet": []byte("{ embedded: 'default-template' }"), - } - mockBlueprintHandler.GetDefaultTemplateDataFunc = func(contextName string) (map[string][]byte, error) { - return expectedDefaultData, nil - } - injector.Register("blueprintHandler", mockBlueprintHandler) - - // Initialize the pipeline to set up all components - if err := pipeline.BasePipeline.Initialize(injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Set artifact builder to nil to test the "no artifact builder" scenario - pipeline.BasePipeline.artifactBuilder = nil - - // When prepareTemplateData is called - templateData, err := pipeline.BasePipeline.prepareTemplateData(context.Background()) - - // Then should use embedded default template - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(templateData) != 1 { - t.Errorf("Expected 1 template file, got %d", len(templateData)) - } - if string(templateData["blueprint.jsonnet"]) != "{ embedded: 'default-template' }" { - t.Error("Expected embedded default template data") - } - }) - - t.Run("ReturnsEmptyMapWhenNothingAvailable", func(t *testing.T) { - // Given a pipeline with no blueprint handler and no artifact builder - pipeline := &InitPipeline{} - - // Set up BasePipeline properly - pipeline.BasePipeline = *NewBasePipeline() - pipeline.BasePipeline.injector = di.NewInjector() - pipeline.BasePipeline.artifactBuilder = nil - - // Mock config handler (needed for determineContextName) - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetContextFunc = func() string { - return "local" - } - pipeline.BasePipeline.configHandler = mockConfigHandler - - // Mock blueprint handler that returns empty data - mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) - mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { - return make(map[string][]byte), nil // Empty local templates - } - mockBlueprintHandler.GetDefaultTemplateDataFunc = func(contextName string) (map[string][]byte, error) { - return make(map[string][]byte), nil // Empty default templates - } - pipeline.BasePipeline.injector.Register("blueprintHandler", mockBlueprintHandler) - - // When prepareTemplateData is called - templateData, err := pipeline.BasePipeline.prepareTemplateData(context.Background()) - - // Then should return empty map - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if templateData == nil { - t.Error("Expected non-nil template data") - } - if len(templateData) != 0 { - t.Error("Expected empty template data") - } - }) - - t.Run("HandlesInvalidOCIReference", func(t *testing.T) { - // Given a pipeline with invalid OCI reference - pipeline := &InitPipeline{} - - // Set up BasePipeline properly - pipeline.BasePipeline = *NewBasePipeline() - pipeline.BasePipeline.injector = di.NewInjector() - - mockArtifactBuilder := artifact.NewMockArtifact() - pipeline.BasePipeline.artifactBuilder = mockArtifactBuilder - - // Create context with invalid blueprint value - ctx := context.WithValue(context.Background(), "blueprint", "invalid-oci-reference") - - // When prepareTemplateData is called - templateData, err := pipeline.BasePipeline.prepareTemplateData(ctx) - - // Then should return error for invalid reference - if err == nil { - t.Fatal("Expected error for invalid OCI reference, got nil") - } - if !strings.Contains(err.Error(), "failed to parse blueprint reference") { - t.Errorf("Expected error to contain 'failed to parse blueprint reference', got %v", err) - } - if templateData != nil { - t.Error("Expected nil template data on error") - } - }) -} - -func TestInitPipeline_setDefaultConfiguration_HostPortsValidation(t *testing.T) { - setup := func(t *testing.T, vmDriver string) (*InitPipeline, *config.MockConfigHandler, *v1alpha1.Context) { - t.Helper() - pipeline := &InitPipeline{} - - mockConfigHandler := config.NewMockConfigHandler() - - // Track which default config was set - var setDefaultConfig v1alpha1.Context - - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - if key == "vm.driver" { - return vmDriver - } - return "" - } - mockConfigHandler.SetDefaultFunc = func(defaultConfig v1alpha1.Context) error { - setDefaultConfig = defaultConfig - return nil - } - mockConfigHandler.SetFunc = func(key string, value interface{}) error { - return nil - } - - pipeline.configHandler = mockConfigHandler - - return pipeline, mockConfigHandler, &setDefaultConfig - } - - t.Run("ColimaDriver_UsesConfigWithoutHostPorts", func(t *testing.T) { - // Given a pipeline with colima driver - pipeline, _, setConfigPtr := setup(t, "colima") - - // When setDefaultConfiguration is called - err := pipeline.setDefaultConfiguration(context.Background(), "test") - - // Then no error should occur - if err != nil { - t.Fatalf("Expected no error, got %v", err) - } - - // And the default config should be DefaultConfig_Full (no hostports) - setConfig := *setConfigPtr - if setConfig.Cluster == nil { - t.Fatal("Expected cluster configuration to be present") - } - - // Verify no hostports for workers - if len(setConfig.Cluster.Workers.HostPorts) != 0 { - t.Errorf("Expected no hostports for colima driver, got %d: %v", - len(setConfig.Cluster.Workers.HostPorts), setConfig.Cluster.Workers.HostPorts) - } - - // Verify no hostports for controlplanes - if len(setConfig.Cluster.ControlPlanes.HostPorts) != 0 { - t.Errorf("Expected no hostports for colima driver controlplanes, got %d: %v", - len(setConfig.Cluster.ControlPlanes.HostPorts), setConfig.Cluster.ControlPlanes.HostPorts) - } - }) - - t.Run("DockerDriver_UsesConfigWithoutHostPorts", func(t *testing.T) { - // Given a pipeline with docker driver - pipeline, _, setConfigPtr := setup(t, "docker") - - // When setDefaultConfiguration is called - err := pipeline.setDefaultConfiguration(context.Background(), "test") - - // Then no error should occur - if err != nil { - t.Fatalf("Expected no error, got %v", err) - } - - // And the default config should be DefaultConfig_Full (no hostports) - setConfig := *setConfigPtr - if setConfig.Cluster == nil { - t.Fatal("Expected cluster configuration to be present") - } - - // Verify no hostports for workers - if len(setConfig.Cluster.Workers.HostPorts) != 0 { - t.Errorf("Expected no hostports for docker driver, got %d: %v", - len(setConfig.Cluster.Workers.HostPorts), setConfig.Cluster.Workers.HostPorts) - } - - // Verify no hostports for controlplanes - if len(setConfig.Cluster.ControlPlanes.HostPorts) != 0 { - t.Errorf("Expected no hostports for docker driver controlplanes, got %d: %v", - len(setConfig.Cluster.ControlPlanes.HostPorts), setConfig.Cluster.ControlPlanes.HostPorts) - } - }) - - t.Run("DockerDesktopDriver_UsesConfigWithHostPorts", func(t *testing.T) { - // Given a pipeline with docker-desktop driver - pipeline, _, setConfigPtr := setup(t, "docker-desktop") - - // When setDefaultConfiguration is called - err := pipeline.setDefaultConfiguration(context.Background(), "test") - - // Then no error should occur - if err != nil { - t.Fatalf("Expected no error, got %v", err) - } - - // And the default config should be DefaultConfig_Localhost (with hostports) - setConfig := *setConfigPtr - if setConfig.Cluster == nil { - t.Fatal("Expected cluster configuration to be present") - } - - // Verify hostports are present for workers - expectedHostPorts := []string{"8080:30080/tcp", "8443:30443/tcp", "9292:30292/tcp", "8053:30053/udp"} - actualHostPorts := setConfig.Cluster.Workers.HostPorts - - if len(actualHostPorts) != len(expectedHostPorts) { - t.Errorf("Expected %d hostports for docker-desktop driver, got %d", - len(expectedHostPorts), len(actualHostPorts)) - } - - for i, expected := range expectedHostPorts { - if i >= len(actualHostPorts) || actualHostPorts[i] != expected { - t.Errorf("Expected hostport %s at index %d, got %s", expected, i, - func() string { - if i < len(actualHostPorts) { - return actualHostPorts[i] - } - return "missing" - }()) - } - } - - // Verify no hostports for controlplanes (only workers need them) - if len(setConfig.Cluster.ControlPlanes.HostPorts) != 0 { - t.Errorf("Expected no hostports for docker-desktop driver controlplanes, got %d: %v", - len(setConfig.Cluster.ControlPlanes.HostPorts), setConfig.Cluster.ControlPlanes.HostPorts) - } - }) -} diff --git a/pkg/pipelines/install.go b/pkg/pipelines/install.go deleted file mode 100644 index 485e3ff3b..000000000 --- a/pkg/pipelines/install.go +++ /dev/null @@ -1,128 +0,0 @@ -package pipelines - -import ( - "context" - "fmt" - - "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/generators" - "github.com/windsorcli/cli/pkg/composer/artifact" - "github.com/windsorcli/cli/pkg/composer/blueprint" -) - -// The InstallPipeline is a specialized component that manages blueprint installation functionality. -// It provides install-specific command execution for blueprint installation with optional waiting -// for kustomizations to be ready. The InstallPipeline assumes that the env pipeline has already -// been executed to handle environment variables and secrets setup. - -// ============================================================================= -// Types -// ============================================================================= - -// InstallPipeline provides blueprint installation functionality -type InstallPipeline struct { - BasePipeline - blueprintHandler blueprint.BlueprintHandler - generators []generators.Generator - artifactBuilder artifact.Artifact -} - -// ============================================================================= -// Constructor -// ============================================================================= - -// NewInstallPipeline creates a new InstallPipeline instance -func NewInstallPipeline() *InstallPipeline { - return &InstallPipeline{ - BasePipeline: *NewBasePipeline(), - } -} - -// ============================================================================= -// Public Methods -// ============================================================================= - -// Initialize configures the InstallPipeline by setting up the blueprint handler, template renderer, -// and generators required for blueprint installation. Only components necessary for blueprint installation -// are initialized, as environment setup is handled by the env pipeline. The method initializes the -// Kubernetes manager and client, blueprint handler, template renderer, and generators, and ensures -// all are properly initialized before use. Returns an error if any component fails to initialize. -func (p *InstallPipeline) Initialize(injector di.Injector, ctx context.Context) error { - if err := p.BasePipeline.Initialize(injector, ctx); err != nil { - return err - } - - kubernetesManager := p.withKubernetesManager() - _ = p.withKubernetesClient() - p.blueprintHandler = p.withBlueprintHandler() - p.artifactBuilder = p.withArtifactBuilder() - generators, err := p.withGenerators() - if err != nil { - return fmt.Errorf("failed to set up generators: %w", err) - } - p.generators = generators - - if kubernetesManager != nil { - if err := kubernetesManager.Initialize(); err != nil { - return fmt.Errorf("failed to initialize kubernetes manager: %w", err) - } - } - - if p.blueprintHandler != nil { - if err := p.blueprintHandler.Initialize(); err != nil { - return fmt.Errorf("failed to initialize blueprint handler: %w", err) - } - } - - for _, generator := range p.generators { - if err := generator.Initialize(); err != nil { - return fmt.Errorf("failed to initialize generator: %w", err) - } - } - - return nil -} - -// Execute runs the blueprint installation process for the InstallPipeline. -// It processes templates for kustomize data, installs the blueprint using the configured blueprint handler, -// and, if the "wait" flag is set in the context, waits for kustomizations to become ready. -// Returns an error if configuration is not loaded, the blueprint handler is missing, installation fails, -// or waiting for kustomizations fails. -func (p *InstallPipeline) Execute(ctx context.Context) error { - if !p.configHandler.IsLoaded() { - return fmt.Errorf("Nothing to install. Have you run \033[1mwindsor init\033[0m?") - } - - if p.blueprintHandler == nil { - return fmt.Errorf("No blueprint handler found") - } - - // Phase 1: Load blueprint config - if err := p.blueprintHandler.LoadConfig(); err != nil { - return fmt.Errorf("Error loading blueprint config: %w", err) - } - - // Phase 2: Generate files using generators - for _, generator := range p.generators { - if err := generator.Generate(map[string]any{}, false); err != nil { - return fmt.Errorf("failed to generate from template data: %w", err) - } - } - - // Phase 3: Install blueprint - if err := p.blueprintHandler.Install(); err != nil { - return fmt.Errorf("Error installing blueprint: %w", err) - } - - // Phase 4: Wait for kustomizations if requested - waitFlag := ctx.Value("wait") - if waitFlag != nil { - if wait, ok := waitFlag.(bool); ok && wait { - if err := p.blueprintHandler.WaitForKustomizations("⏳ Waiting for kustomizations to be ready"); err != nil { - return fmt.Errorf("failed waiting for kustomizations: %w", err) - } - } - } - - return nil -} diff --git a/pkg/pipelines/install_test.go b/pkg/pipelines/install_test.go deleted file mode 100644 index 8df3c16ff..000000000 --- a/pkg/pipelines/install_test.go +++ /dev/null @@ -1,640 +0,0 @@ -package pipelines - -import ( - "context" - "fmt" - "strings" - "testing" - - "github.com/windsorcli/cli/pkg/context/config" - "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/composer/artifact" - "github.com/windsorcli/cli/pkg/composer/blueprint" -) - -// ============================================================================= -// Mock Types -// ============================================================================= - -type MockTemplate struct { - ProcessCalled bool - ProcessFunc func(templateData map[string][]byte, renderedData map[string]any) error -} - -func (m *MockTemplate) Initialize() error { return nil } -func (m *MockTemplate) Process(templateData map[string][]byte, renderedData map[string]any) error { - m.ProcessCalled = true - if m.ProcessFunc != nil { - return m.ProcessFunc(templateData, renderedData) - } - return nil -} - -type MockGenerator struct { - GenerateCalled bool - GenerateFunc func(data map[string]any, overwrite ...bool) error -} - -func (m *MockGenerator) Initialize() error { return nil } -func (m *MockGenerator) Generate(data map[string]any, overwrite ...bool) error { - m.GenerateCalled = true - if m.GenerateFunc != nil { - return m.GenerateFunc(data, overwrite...) - } - return nil -} - -// ============================================================================= -// Test Setup -// ============================================================================= - -type InstallMocks struct { - *Mocks - BlueprintHandler *blueprint.MockBlueprintHandler -} - -func setupInstallMocks(t *testing.T, opts ...*SetupOptions) *InstallMocks { - t.Helper() - - // Create setup options, preserving any provided options - setupOptions := &SetupOptions{} - if len(opts) > 0 && opts[0] != nil { - setupOptions = opts[0] - } - - baseMocks := setupMocks(t, setupOptions) - - // Setup blueprint handler mock - mockBlueprintHandler := blueprint.NewMockBlueprintHandler(baseMocks.Injector) - mockBlueprintHandler.InitializeFunc = func() error { return nil } - mockBlueprintHandler.InstallFunc = func() error { return nil } - mockBlueprintHandler.WaitForKustomizationsFunc = func(message string, names ...string) error { return nil } - baseMocks.Injector.Register("blueprintHandler", mockBlueprintHandler) - - // Add artifact builder mock for generators - artifactBuilder := artifact.NewMockArtifact() - artifactBuilder.InitializeFunc = func(injector di.Injector) error { return nil } - baseMocks.Injector.Register("artifactBuilder", artifactBuilder) - - return &InstallMocks{ - Mocks: baseMocks, - BlueprintHandler: mockBlueprintHandler, - } -} - -// ============================================================================= -// Test Constructor -// ============================================================================= - -func TestNewInstallPipeline(t *testing.T) { - t.Run("CreatesNewInstallPipeline", func(t *testing.T) { - // When creating a new InstallPipeline - pipeline := NewInstallPipeline() - - // Then it should not be nil - if pipeline == nil { - t.Error("Expected pipeline to not be nil") - } - - // And it should be of the correct type - if pipeline == nil { - t.Error("Expected pipeline to be of type *InstallPipeline") - } - }) -} - -// ============================================================================= -// Test Initialize -// ============================================================================= - -func TestInstallPipeline_Initialize(t *testing.T) { - setup := func(t *testing.T, opts ...*SetupOptions) (*InstallPipeline, *InstallMocks) { - t.Helper() - pipeline := NewInstallPipeline() - mocks := setupInstallMocks(t, opts...) - return pipeline, mocks - } - - t.Run("InitializesSuccessfully", func(t *testing.T) { - // Given a new InstallPipeline - pipeline, mocks := setup(t) - - // When Initialize is called - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And blueprint handler should be set - if pipeline.blueprintHandler == nil { - t.Error("Expected blueprint handler to be set") - } - }) - - t.Run("ReturnsErrorWhenBasePipelineInitializeFails", func(t *testing.T) { - // Given a pipeline with failing base initialization - pipeline, mocks := setup(t) - - // Override shell to return error during initialization - mocks.Shell.InitializeFunc = func() error { - return fmt.Errorf("shell init failed") - } - - // When Initialize is called - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "failed to initialize shell: shell init failed" { - t.Errorf("Expected shell init error, got %q", err.Error()) - } - }) - - t.Run("ReturnsErrorWhenBlueprintHandlerInitializeFails", func(t *testing.T) { - // Given a pipeline with failing blueprint handler initialization - pipeline, mocks := setup(t) - - // Override blueprint handler to return error during initialization - mocks.BlueprintHandler.InitializeFunc = func() error { - return fmt.Errorf("blueprint handler init failed") - } - - // When Initialize is called - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "failed to initialize blueprint handler: blueprint handler init failed" { - t.Errorf("Expected blueprint handler init error, got %q", err.Error()) - } - }) -} - -// ============================================================================= -// Test Execute -// ============================================================================= - -func TestInstallPipeline_Execute(t *testing.T) { - setup := func(t *testing.T, opts ...*SetupOptions) (*InstallPipeline, *InstallMocks) { - t.Helper() - pipeline := NewInstallPipeline() - mocks := setupInstallMocks(t, opts...) - - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - return pipeline, mocks - } - - t.Run("ExecutesSuccessfully", func(t *testing.T) { - // Given a properly initialized InstallPipeline - pipeline, _ := setup(t) - - // When Execute is called - err := pipeline.Execute(context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("ExecutesWithWaitFlag", func(t *testing.T) { - // Given a pipeline with wait flag set - pipeline, mocks := setup(t) - - waitCalled := false - mocks.BlueprintHandler.WaitForKustomizationsFunc = func(message string, names ...string) error { - waitCalled = true - return nil - } - - ctx := context.WithValue(context.Background(), "wait", true) - - // When Execute is called - err := pipeline.Execute(ctx) - - // Then no error should be returned and wait should be called - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if !waitCalled { - t.Error("Expected blueprint wait to be called") - } - }) - - t.Run("ReturnsErrorWhenConfigNotLoaded", func(t *testing.T) { - // Given a mock config handler that returns not loaded - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.IsLoadedFunc = func() bool { return false } - mockConfigHandler.InitializeFunc = func() error { return nil } - mockConfigHandler.GetContextFunc = func() string { return "mock-context" } - mockConfigHandler.SetContextFunc = func(context string) error { return nil } - - // Setup with the not-loaded config handler - pipeline, _ := setup(t, &SetupOptions{ConfigHandler: mockConfigHandler}) - - // When Execute is called - err := pipeline.Execute(context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "Nothing to install. Have you run \033[1mwindsor init\033[0m?" { - t.Errorf("Expected config not loaded error, got %q", err.Error()) - } - }) - - t.Run("ReturnsErrorWhenNoBlueprintHandler", func(t *testing.T) { - // Given a pipeline with nil blueprint handler - pipeline, _ := setup(t) - - // Set blueprint handler to nil - pipeline.blueprintHandler = nil - - // When Execute is called - err := pipeline.Execute(context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "No blueprint handler found" { - t.Errorf("Expected no blueprint handler error, got %q", err.Error()) - } - }) - - t.Run("ReturnsErrorWhenBlueprintInstallFails", func(t *testing.T) { - // Given a pipeline with failing blueprint install - pipeline, mocks := setup(t) - - // Override blueprint handler to return error during install - mocks.BlueprintHandler.InstallFunc = func() error { - return fmt.Errorf("blueprint install failed") - } - - // When Execute is called - err := pipeline.Execute(context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "Error installing blueprint: blueprint install failed" { - t.Errorf("Expected blueprint install error, got %q", err.Error()) - } - }) - - t.Run("ReturnsErrorWhenBlueprintWaitFails", func(t *testing.T) { - // Given a pipeline with failing blueprint wait - pipeline, mocks := setup(t) - - // Override blueprint handler to return error during wait - mocks.BlueprintHandler.WaitForKustomizationsFunc = func(message string, names ...string) error { - return fmt.Errorf("blueprint wait failed") - } - - ctx := context.WithValue(context.Background(), "wait", true) - - // When Execute is called - err := pipeline.Execute(ctx) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "failed waiting for kustomizations: blueprint wait failed" { - t.Errorf("Expected blueprint wait error, got %q", err.Error()) - } - }) - - t.Run("LoadsBlueprintConfigBeforeInstall", func(t *testing.T) { - // Given a pipeline with blueprint handler - pipeline, mocks := setup(t) - - loadConfigCalled := false - installCalled := false - var callOrder []string - - // Track the order of method calls - mocks.BlueprintHandler.LoadConfigFunc = func() error { - loadConfigCalled = true - callOrder = append(callOrder, "LoadConfig") - return nil - } - - mocks.BlueprintHandler.InstallFunc = func() error { - installCalled = true - callOrder = append(callOrder, "Install") - return nil - } - - // When Execute is called - err := pipeline.Execute(context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And LoadConfig should be called before Install - if !loadConfigCalled { - t.Error("Expected LoadConfig to be called") - } - if !installCalled { - t.Error("Expected Install to be called") - } - - // And LoadConfig should be called before Install - if len(callOrder) < 2 { - t.Errorf("Expected at least 2 method calls, got %d", len(callOrder)) - } - // Find the first LoadConfig call - loadConfigIndex := -1 - installIndex := -1 - for i, call := range callOrder { - if call == "LoadConfig" && loadConfigIndex == -1 { - loadConfigIndex = i - } - if call == "Install" { - installIndex = i - } - } - if loadConfigIndex == -1 { - t.Error("Expected LoadConfig to be called") - } - if installIndex == -1 { - t.Error("Expected Install to be called") - } - if loadConfigIndex >= installIndex { - t.Error("Expected LoadConfig to be called before Install") - } - }) - - t.Run("ReturnsErrorWhenBlueprintLoadConfigFails", func(t *testing.T) { - // Given a pipeline with failing blueprint LoadConfig - pipeline, mocks := setup(t) - - // Override blueprint handler to return error during LoadConfig - mocks.BlueprintHandler.LoadConfigFunc = func() error { - return fmt.Errorf("blueprint load config failed") - } - - // When Execute is called - err := pipeline.Execute(context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "Error loading blueprint config: blueprint load config failed" { - t.Errorf("Expected blueprint load config error, got %q", err.Error()) - } - }) - - t.Run("LoadsBlueprintConfigSuccessfully", func(t *testing.T) { - // Given a pipeline - pipeline, mocks := setup(t) - - // Initialize the pipeline to set up generators - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When Execute is called - err := pipeline.Execute(context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("ReturnsErrorWhenBlueprintLoadConfigFails", func(t *testing.T) { - // Given a pipeline with failing blueprint loading - pipeline, mocks := setup(t) - - // Mock blueprint handler to return error on LoadConfig - mocks.BlueprintHandler.LoadConfigFunc = func() error { - return fmt.Errorf("blueprint load config failed") - } - - // Initialize the pipeline to set up generators - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When Execute is called - err := pipeline.Execute(context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - - // And the error should be about blueprint loading - if !strings.Contains(err.Error(), "failed to load blueprint config") && !strings.Contains(err.Error(), "Error loading blueprint config") { - t.Errorf("Expected blueprint loading error, got %q", err.Error()) - } - }) - - t.Run("ReturnsErrorWhenGeneratorFails", func(t *testing.T) { - // Given a pipeline with failing generator - pipeline, mocks := setup(t) - - // Mock template renderer to return test data - mockTemplateRenderer := &MockTemplate{} - mockTemplateRenderer.ProcessFunc = func(templateData map[string][]byte, renderedData map[string]any) error { - renderedData["kustomize/values"] = map[string]any{ - "common": map[string]any{ - "domain": "test.com", - }, - } - return nil - } - // Register the mock template renderer in the injector BEFORE initialization - mocks.Injector.Register("templateRenderer", mockTemplateRenderer) - - // Initialize the pipeline to set up generators - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Mock blueprint handler to return template data - mocks.BlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { - return map[string][]byte{ - "blueprint.jsonnet": []byte(`{"kustomize": [{"name": "test"}]}`), - }, nil - } - - // Mock generator to return error - for i := range pipeline.generators { - mockGenerator := &MockGenerator{} - mockGenerator.GenerateFunc = func(data map[string]any, overwrite ...bool) error { - return fmt.Errorf("generator failed") - } - pipeline.generators[i] = mockGenerator - } - - // When Execute is called - err := pipeline.Execute(context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "failed to generate from template data: generator failed" { - t.Errorf("Expected generator error, got %q", err.Error()) - } - }) - - t.Run("CallsGeneratorsEvenWithNoRenderedData", func(t *testing.T) { - // Given a pipeline with no rendered data - pipeline, mocks := setup(t) - - // Mock template renderer to return empty data - mockTemplateRenderer := &MockTemplate{} - mockTemplateRenderer.ProcessFunc = func(templateData map[string][]byte, renderedData map[string]any) error { - // Don't add any data to renderedData - return nil - } - // Register the mock template renderer in the injector BEFORE initialization - mocks.Injector.Register("templateRenderer", mockTemplateRenderer) - - // Initialize the pipeline to set up generators - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Mock blueprint handler to return template data - mocks.BlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { - return map[string][]byte{ - "blueprint.jsonnet": []byte(`{"kustomize": [{"name": "test"}]}`), - }, nil - } - - // Track generator calls - generatorCalled := false - for i := range pipeline.generators { - mockGenerator := &MockGenerator{} - mockGenerator.GenerateFunc = func(data map[string]any, overwrite ...bool) error { - generatorCalled = true - return nil - } - pipeline.generators[i] = mockGenerator - } - - // When Execute is called - err := pipeline.Execute(context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And generators should be called even with empty rendered data - if !generatorCalled { - t.Error("Expected generators to be called even when no rendered data") - } - }) - - t.Run("CallsGeneratorsWithNilData", func(t *testing.T) { - // Given a pipeline - pipeline, mocks := setup(t) - - // Initialize the pipeline to set up generators - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Track data passed to generators - var receivedData map[string]any - for i := range pipeline.generators { - mockGenerator := &MockGenerator{} - mockGenerator.GenerateFunc = func(data map[string]any, overwrite ...bool) error { - receivedData = data - return nil - } - pipeline.generators[i] = mockGenerator - } - - // When Execute is called - err := pipeline.Execute(context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And empty data should be passed to generators (since we no longer use template processing) - if receivedData == nil || len(receivedData) != 0 { - t.Errorf("Expected empty data to be passed to generators, got %v", receivedData) - } - }) - - t.Run("PassesCorrectOverwriteFlagToGenerators", func(t *testing.T) { - // Given a pipeline with rendered data - pipeline, mocks := setup(t) - - // Mock template renderer to return test data - mockTemplateRenderer := &MockTemplate{} - mockTemplateRenderer.ProcessFunc = func(templateData map[string][]byte, renderedData map[string]any) error { - renderedData["kustomize/values"] = map[string]any{ - "common": map[string]any{ - "domain": "test.com", - }, - } - return nil - } - // Register the mock template renderer in the injector BEFORE initialization - mocks.Injector.Register("templateRenderer", mockTemplateRenderer) - - // Initialize the pipeline to set up generators - if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Mock blueprint handler to return template data - mocks.BlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { - return map[string][]byte{ - "blueprint.jsonnet": []byte(`{"kustomize": [{"name": "test"}]}`), - }, nil - } - - // Track overwrite flag passed to generators - var receivedOverwrite []bool - for i := range pipeline.generators { - mockGenerator := &MockGenerator{} - mockGenerator.GenerateFunc = func(data map[string]any, overwrite ...bool) error { - receivedOverwrite = overwrite - return nil - } - pipeline.generators[i] = mockGenerator - } - - // When Execute is called - err := pipeline.Execute(context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And overwrite flag should be false - if len(receivedOverwrite) != 1 { - t.Errorf("Expected 1 overwrite flag, got %d", len(receivedOverwrite)) - } - if receivedOverwrite[0] != false { - t.Errorf("Expected overwrite flag to be false, got %v", receivedOverwrite[0]) - } - }) -} diff --git a/pkg/pipelines/mock_pipeline.go b/pkg/pipelines/mock_pipeline.go deleted file mode 100644 index 207cc8cae..000000000 --- a/pkg/pipelines/mock_pipeline.go +++ /dev/null @@ -1,45 +0,0 @@ -package pipelines - -import ( - "context" - - "github.com/windsorcli/cli/pkg/di" -) - -// MockBasePipeline is a mock implementation of the Pipeline interface -type MockBasePipeline struct { - InitializeFunc func(injector di.Injector, ctx context.Context) error - ExecuteFunc func(ctx context.Context) error -} - -// ============================================================================= -// Constructor -// ============================================================================= - -// NewMockBasePipeline creates a new MockBasePipeline instance -func NewMockBasePipeline() *MockBasePipeline { - return &MockBasePipeline{} -} - -// ============================================================================= -// Public Methods -// ============================================================================= - -// Initialize calls the mock InitializeFunc if set, otherwise returns nil -func (m *MockBasePipeline) Initialize(injector di.Injector, ctx context.Context) error { - if m.InitializeFunc != nil { - return m.InitializeFunc(injector, ctx) - } - return nil -} - -// Execute calls the mock ExecuteFunc if set, otherwise returns nil -func (m *MockBasePipeline) Execute(ctx context.Context) error { - if m.ExecuteFunc != nil { - return m.ExecuteFunc(ctx) - } - return nil -} - -// Ensure MockBasePipeline implements Pipeline -var _ Pipeline = (*MockBasePipeline)(nil) diff --git a/pkg/pipelines/mock_pipeline_test.go b/pkg/pipelines/mock_pipeline_test.go deleted file mode 100644 index e78a7a73b..000000000 --- a/pkg/pipelines/mock_pipeline_test.go +++ /dev/null @@ -1,185 +0,0 @@ -package pipelines - -import ( - "context" - "fmt" - "testing" - - "github.com/windsorcli/cli/pkg/di" -) - -// ============================================================================= -// Test Setup -// ============================================================================= - -func setupMockBasePipeline(t *testing.T) *MockBasePipeline { - t.Helper() - - pipeline := NewMockBasePipeline() - - return pipeline -} - -// ============================================================================= -// Test Constructor -// ============================================================================= - -func TestNewMockBasePipeline(t *testing.T) { - t.Run("CreatesWithDefaults", func(t *testing.T) { - // Given creating a new mock base pipeline - pipeline := NewMockBasePipeline() - - // Then pipeline should not be nil - if pipeline == nil { - t.Fatal("Expected pipeline to not be nil") - } - }) -} - -// ============================================================================= -// Test Public Methods -// ============================================================================= - -func TestMockBasePipeline_Initialize(t *testing.T) { - t.Run("CallsInitializeFuncWhenSet", func(t *testing.T) { - // Given a mock pipeline with custom initialize function - pipeline := setupMockBasePipeline(t) - - initializeCalled := false - var capturedInjector di.Injector - var capturedCtx context.Context - pipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { - initializeCalled = true - capturedInjector = injector - capturedCtx = ctx - return nil - } - - injector := di.NewInjector() - ctx := context.Background() - - // When initializing the pipeline - err := pipeline.Initialize(injector, ctx) - - // Then the custom function should be called - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if !initializeCalled { - t.Error("Expected InitializeFunc to be called") - } - if capturedInjector != injector { - t.Error("Expected injector to be passed to InitializeFunc") - } - if capturedCtx != ctx { - t.Error("Expected context to be passed to InitializeFunc") - } - }) - - t.Run("ReturnsErrorWhenInitializeFuncFails", func(t *testing.T) { - // Given a mock pipeline with failing initialize function - pipeline := setupMockBasePipeline(t) - - pipeline.InitializeFunc = func(injector di.Injector, ctx context.Context) error { - return fmt.Errorf("initialize failed") - } - - injector := di.NewInjector() - ctx := context.Background() - - // When initializing the pipeline - err := pipeline.Initialize(injector, ctx) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "initialize failed" { - t.Errorf("Expected 'initialize failed', got: %v", err) - } - }) - - t.Run("ReturnsNilWhenInitializeFuncNotSet", func(t *testing.T) { - // Given a mock pipeline without custom initialize function - pipeline := setupMockBasePipeline(t) - - injector := di.NewInjector() - ctx := context.Background() - - // When initializing the pipeline - err := pipeline.Initialize(injector, ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) -} - -func TestMockBasePipeline_Execute(t *testing.T) { - t.Run("CallsExecuteFuncWhenSet", func(t *testing.T) { - // Given a mock pipeline with custom execute function - pipeline := setupMockBasePipeline(t) - - executeCalled := false - var capturedCtx context.Context - pipeline.ExecuteFunc = func(ctx context.Context) error { - executeCalled = true - capturedCtx = ctx - return nil - } - - ctx := context.Background() - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then the custom function should be called - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if !executeCalled { - t.Error("Expected ExecuteFunc to be called") - } - if capturedCtx != ctx { - t.Error("Expected context to be passed to ExecuteFunc") - } - }) - - t.Run("ReturnsErrorWhenExecuteFuncFails", func(t *testing.T) { - // Given a mock pipeline with failing execute function - pipeline := setupMockBasePipeline(t) - - pipeline.ExecuteFunc = func(ctx context.Context) error { - return fmt.Errorf("execute failed") - } - - ctx := context.Background() - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "execute failed" { - t.Errorf("Expected 'execute failed', got: %v", err) - } - }) - - t.Run("ReturnsNilWhenExecuteFuncNotSet", func(t *testing.T) { - // Given a mock pipeline without custom execute function - pipeline := setupMockBasePipeline(t) - - ctx := context.Background() - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) -} diff --git a/pkg/pipelines/pipeline.go b/pkg/pipelines/pipeline.go deleted file mode 100644 index 8c0ed9c6b..000000000 --- a/pkg/pipelines/pipeline.go +++ /dev/null @@ -1,748 +0,0 @@ -package pipelines - -import ( - "context" - "fmt" - "os" - "path/filepath" - - secretsConfigType "github.com/windsorcli/cli/api/v1alpha1/secrets" - "github.com/windsorcli/cli/pkg/context/config" - "github.com/windsorcli/cli/pkg/constants" - "github.com/windsorcli/cli/pkg/di" - envpkg "github.com/windsorcli/cli/pkg/context/env" - "github.com/windsorcli/cli/pkg/context/tools" - "github.com/windsorcli/cli/pkg/generators" - "github.com/windsorcli/cli/pkg/provisioner/cluster" - "github.com/windsorcli/cli/pkg/provisioner/kubernetes" - k8sclient "github.com/windsorcli/cli/pkg/provisioner/kubernetes/client" - terraforminfra "github.com/windsorcli/cli/pkg/provisioner/terraform" - "github.com/windsorcli/cli/pkg/composer/artifact" - "github.com/windsorcli/cli/pkg/composer/blueprint" - "github.com/windsorcli/cli/pkg/composer/terraform" - "github.com/windsorcli/cli/pkg/context/secrets" - "github.com/windsorcli/cli/pkg/context/shell" - "github.com/windsorcli/cli/pkg/context/shell/ssh" - "github.com/windsorcli/cli/pkg/workstation/network" - "github.com/windsorcli/cli/pkg/workstation/services" - "github.com/windsorcli/cli/pkg/workstation/virt" -) - -// The BasePipeline is a foundational component that provides common pipeline functionality for command execution. -// It provides a unified interface for pipeline execution with dependency injection support, -// serving as the base implementation for all command-specific pipelines in the Windsor CLI system. -// The BasePipeline facilitates standardized command execution patterns with direct dependency injection. - -// ============================================================================= -// Types -// ============================================================================= - -// Pipeline defines the interface for all command pipelines -type Pipeline interface { - Initialize(injector di.Injector, ctx context.Context) error - Execute(ctx context.Context) error -} - -// PipelineConstructor defines a function that creates a new pipeline instance -type PipelineConstructor func() Pipeline - -// ============================================================================= -// Pipeline Factory -// ============================================================================= - -// pipelineConstructors maps pipeline names to their constructor functions -var pipelineConstructors = map[string]PipelineConstructor{ - "initPipeline": func() Pipeline { return NewInitPipeline() }, - "execPipeline": func() Pipeline { return NewExecPipeline() }, - "checkPipeline": func() Pipeline { return NewCheckPipeline() }, - "upPipeline": func() Pipeline { return NewUpPipeline() }, - "downPipeline": func() Pipeline { return NewDownPipeline() }, - "installPipeline": func() Pipeline { return NewInstallPipeline() }, - "buildIDPipeline": func() Pipeline { return NewBuildIDPipeline() }, - "basePipeline": func() Pipeline { return NewBasePipeline() }, -} - -// WithPipeline resolves or creates a pipeline instance from the DI container by name. -// If the pipeline already exists in the injector, it is returned directly. Otherwise, a new instance is constructed, -// initialized with the provided injector and context, registered in the DI container, and then returned. -// Returns an error if the pipeline name is unknown or initialization fails. -func WithPipeline(injector di.Injector, ctx context.Context, pipelineName string) (Pipeline, error) { - if existing := injector.Resolve(pipelineName); existing != nil { - if pipeline, ok := existing.(Pipeline); ok { - return pipeline, nil - } - } - - constructor, exists := pipelineConstructors[pipelineName] - if !exists { - return nil, fmt.Errorf("unknown pipeline: %s", pipelineName) - } - - pipeline := constructor() - - if err := pipeline.Initialize(injector, ctx); err != nil { - return nil, fmt.Errorf("failed to initialize %s: %w", pipelineName, err) - } - - injector.Register(pipelineName, pipeline) - - return pipeline, nil -} - -// BasePipeline provides common pipeline functionality including config loading and template processing -// Specific pipelines should embed this and add their own dependencies -type BasePipeline struct { - shell shell.Shell - configHandler config.ConfigHandler - shims *Shims - injector di.Injector - artifactBuilder artifact.Artifact - blueprintHandler blueprint.BlueprintHandler -} - -// ============================================================================= -// Constructor -// ============================================================================= - -// NewBasePipeline creates a new BasePipeline instance -func NewBasePipeline() *BasePipeline { - return &BasePipeline{} -} - -// ============================================================================= -// Public Methods -// ============================================================================= - -// Initialize sets up the base pipeline components including dependency injection container, -// shell interface, configuration handler, and shims. It resolves dependencies from the DI -// container and initializes core components required for pipeline execution. -func (p *BasePipeline) Initialize(injector di.Injector, ctx context.Context) error { - p.injector = injector - - p.shell = p.withShell() - p.configHandler = p.withConfigHandler() - p.shims = p.withShims() - p.artifactBuilder = p.withArtifactBuilder() - p.blueprintHandler = p.withBlueprintHandler() - - if err := p.shell.Initialize(); err != nil { - return fmt.Errorf("failed to initialize shell: %w", err) - } - - // Add current directory to trusted file if trust context is set - if trust, ok := ctx.Value("trust").(bool); ok && trust { - if err := p.shell.AddCurrentDirToTrustedFile(); err != nil { - return fmt.Errorf("failed to add current directory to trusted file: %w", err) - } - } - - // Set shell verbosity based on context - if verbose, ok := ctx.Value("verbose").(bool); ok && verbose { - p.shell.SetVerbosity(true) - } - - if err := p.configHandler.Initialize(); err != nil { - return fmt.Errorf("failed to initialize config handler: %w", err) - } - - // Load base config first - if err := p.loadBaseConfig(); err != nil { - return fmt.Errorf("failed to load base config: %w", err) - } - - // Set Windsor context if specified in execution context - if contextName, ok := ctx.Value("contextName").(string); ok && contextName != "" { - if err := p.configHandler.SetContext(contextName); err != nil { - return fmt.Errorf("failed to set Windsor context: %w", err) - } - } - - // Load context config after context is set (only if not in init pipeline) - // Init pipeline doesn't load config because files don't exist yet - isInit, _ := ctx.Value("initPipeline").(bool) - if !isInit { - if err := p.configHandler.LoadConfig(); err != nil { - return fmt.Errorf("failed to load context config: %w", err) - } - } else { - } - - return nil -} - -// Execute provides a default implementation that can be overridden by specific pipelines -func (p *BasePipeline) Execute(ctx context.Context) error { - return nil -} - -// ============================================================================= -// Private Methods -// ============================================================================= - -// withShell resolves or creates shell from DI container -func (p *BasePipeline) withShell() shell.Shell { - if existing := p.injector.Resolve("shell"); existing != nil { - if shell, ok := existing.(shell.Shell); ok { - p.shell = shell - return shell - } - } - - p.shell = shell.NewDefaultShell(p.injector) - p.injector.Register("shell", p.shell) - return p.shell -} - -// withConfigHandler resolves or creates config handler from DI container -func (p *BasePipeline) withConfigHandler() config.ConfigHandler { - if existing := p.injector.Resolve("configHandler"); existing != nil { - if configHandler, ok := existing.(config.ConfigHandler); ok { - p.configHandler = configHandler - return configHandler - } - } - - p.configHandler = config.NewConfigHandler(p.injector) - p.injector.Register("configHandler", p.configHandler) - return p.configHandler -} - -// withShims resolves or creates shims from DI container -func (p *BasePipeline) withShims() *Shims { - if existing := p.injector.Resolve("shims"); existing != nil { - if shims, ok := existing.(*Shims); ok { - p.shims = shims - return shims - } - } - - p.shims = NewShims() - p.injector.Register("shims", p.shims) - return p.shims -} - -// withToolsManager resolves or creates tools manager from DI container -func (p *BasePipeline) withToolsManager() tools.ToolsManager { - if existing := p.injector.Resolve("toolsManager"); existing != nil { - if toolsManager, ok := existing.(tools.ToolsManager); ok { - return toolsManager - } - } - - toolsManager := tools.NewToolsManager(p.injector) - p.injector.Register("toolsManager", toolsManager) - return toolsManager -} - -// withClusterClient resolves or creates cluster client from DI container -func (p *BasePipeline) withClusterClient() cluster.ClusterClient { - if existing := p.injector.Resolve("clusterClient"); existing != nil { - if clusterClient, ok := existing.(cluster.ClusterClient); ok { - return clusterClient - } - } - - clusterClient := cluster.NewTalosClusterClient(p.injector) - p.injector.Register("clusterClient", clusterClient) - return clusterClient -} - -// withBlueprintHandler resolves or creates blueprint handler from DI container -func (p *BasePipeline) withBlueprintHandler() blueprint.BlueprintHandler { - if existing := p.injector.Resolve("blueprintHandler"); existing != nil { - if handler, ok := existing.(blueprint.BlueprintHandler); ok { - return handler - } - } - - handler := blueprint.NewBlueprintHandler(p.injector) - p.injector.Register("blueprintHandler", handler) - return handler -} - -// withStack resolves or creates stack from DI container -func (p *BasePipeline) withStack() terraforminfra.Stack { - if existing := p.injector.Resolve("stack"); existing != nil { - if stack, ok := existing.(terraforminfra.Stack); ok { - return stack - } - } - - stack := terraforminfra.NewWindsorStack(p.injector) - p.injector.Register("stack", stack) - return stack -} - -// withGenerators creates and registers generator instances for git and terraform based on configuration. -// It always registers the git generator. The terraform generator is registered if "terraform.enabled" is true. -// Returns a slice of initialized generators or an error. -func (p *BasePipeline) withGenerators() ([]generators.Generator, error) { - var generatorList []generators.Generator - - gitGenerator := generators.NewGitGenerator(p.injector) - p.injector.Register("gitGenerator", gitGenerator) - generatorList = append(generatorList, gitGenerator) - - if p.configHandler.GetBool("terraform.enabled", false) { - terraformGenerator := generators.NewTerraformGenerator(p.injector) - p.injector.Register("terraformGenerator", terraformGenerator) - generatorList = append(generatorList, terraformGenerator) - } - - return generatorList, nil -} - -// withArtifactBuilder resolves or creates artifact builder from DI container -func (p *BasePipeline) withArtifactBuilder() artifact.Artifact { - if existing := p.injector.Resolve("artifactBuilder"); existing != nil { - if builder, ok := existing.(artifact.Artifact); ok { - return builder - } - } - - builder := artifact.NewArtifactBuilder() - p.injector.Register("artifactBuilder", builder) - return builder -} - -// withVirtualMachine resolves or creates virtual machine from DI container -func (p *BasePipeline) withVirtualMachine() virt.VirtualMachine { - vmDriver := p.configHandler.GetString("vm.driver") - if vmDriver == "" { - return nil - } - - if existing := p.injector.Resolve("virtualMachine"); existing != nil { - if vm, ok := existing.(virt.VirtualMachine); ok { - return vm - } - } - - if vmDriver == "colima" { - vm := virt.NewColimaVirt(p.injector) - p.injector.Register("virtualMachine", vm) - return vm - } - - return nil -} - -// withContainerRuntime resolves or creates container runtime from DI container -func (p *BasePipeline) withContainerRuntime() virt.ContainerRuntime { - if existing := p.injector.Resolve("containerRuntime"); existing != nil { - if containerRuntime, ok := existing.(virt.ContainerRuntime); ok { - return containerRuntime - } - } - - // Only create docker runtime if docker is enabled - if !p.configHandler.GetBool("docker.enabled", false) { - return nil - } - - containerRuntime := virt.NewDockerVirt(p.injector) - p.injector.Register("containerRuntime", containerRuntime) - return containerRuntime -} - -// withKubernetesClient resolves or creates kubernetes client from DI container -func (p *BasePipeline) withKubernetesClient() k8sclient.KubernetesClient { - if existing := p.injector.Resolve("kubernetesClient"); existing != nil { - if kubernetesClient, ok := existing.(k8sclient.KubernetesClient); ok { - return kubernetesClient - } - } - - kubernetesClient := k8sclient.NewDynamicKubernetesClient() - p.injector.Register("kubernetesClient", kubernetesClient) - return kubernetesClient -} - -// withKubernetesManager resolves or creates kubernetes manager from DI container -func (p *BasePipeline) withKubernetesManager() kubernetes.KubernetesManager { - if existing := p.injector.Resolve("kubernetesManager"); existing != nil { - if kubernetesManager, ok := existing.(kubernetes.KubernetesManager); ok { - return kubernetesManager - } - } - - kubernetesManager := kubernetes.NewKubernetesManager(p.injector) - p.injector.Register("kubernetesManager", kubernetesManager) - return kubernetesManager -} - -// withNetworking resolves or creates all networking components from DI container -func (p *BasePipeline) withNetworking() network.NetworkManager { - // Check if network manager already exists - if existing := p.injector.Resolve("networkManager"); existing != nil { - if networkManager, ok := existing.(network.NetworkManager); ok { - return networkManager - } - } - - // Ensure SSH client is registered - if existing := p.injector.Resolve("sshClient"); existing == nil { - sshClient := ssh.NewSSHClient() - p.injector.Register("sshClient", sshClient) - } - - // Ensure secure shell is registered - if existing := p.injector.Resolve("secureShell"); existing == nil { - secureShell := shell.NewSecureShell(p.injector) - p.injector.Register("secureShell", secureShell) - } - - // Ensure network interface provider is registered - if existing := p.injector.Resolve("networkInterfaceProvider"); existing == nil { - networkInterfaceProvider := network.NewNetworkInterfaceProvider() - p.injector.Register("networkInterfaceProvider", networkInterfaceProvider) - } - - // Create and register network manager - vmDriver := p.configHandler.GetString("vm.driver") - var networkManager network.NetworkManager - - if vmDriver == "colima" { - networkManager = network.NewColimaNetworkManager(p.injector) - } else { - networkManager = network.NewBaseNetworkManager(p.injector) - } - - p.injector.Register("networkManager", networkManager) - return networkManager -} - -// handleSessionReset checks session state and performs reset if needed. -// This is a common pattern used by multiple commands (env, exec, context, init). -func (p *BasePipeline) handleSessionReset() error { - if p.shell == nil { - return nil - } - - hasSessionToken := os.Getenv("WINDSOR_SESSION_TOKEN") != "" - - shouldReset, err := p.shell.CheckResetFlags() - if err != nil { - return err - } - - if !hasSessionToken { - shouldReset = true - } - - if shouldReset { - p.shell.Reset() - - if err := os.Setenv("NO_CACHE", "true"); err != nil { - return err - } - } - - return nil -} - -// loadBaseConfig loads only the base configuration file (windsor.yaml) without loading context config -func (p *BasePipeline) loadBaseConfig() error { - if p.shell == nil { - return fmt.Errorf("shell not initialized") - } - if p.configHandler == nil { - return fmt.Errorf("config handler not initialized") - } - if p.shims == nil { - return fmt.Errorf("shims not initialized") - } - - // Config is now loaded via LoadConfig() which loads from standard paths - // Root windsor.yaml loading is handled by LoadConfig() - - return nil -} - -// withEnvPrinters creates environment printers based on configuration -func (p *BasePipeline) withEnvPrinters() ([]envpkg.EnvPrinter, error) { - if p.configHandler == nil { - return nil, fmt.Errorf("config handler not initialized") - } - - var envPrinters []envpkg.EnvPrinter - - if p.configHandler.GetBool("aws.enabled", false) { - awsEnv := envpkg.NewAwsEnvPrinter(p.injector) - envPrinters = append(envPrinters, awsEnv) - p.injector.Register("awsEnv", awsEnv) - } - - if p.configHandler.GetBool("azure.enabled", false) { - azureEnv := envpkg.NewAzureEnvPrinter(p.injector) - envPrinters = append(envPrinters, azureEnv) - p.injector.Register("azureEnv", azureEnv) - } - - if p.configHandler.GetBool("docker.enabled", false) { - dockerEnv := envpkg.NewDockerEnvPrinter(p.injector) - envPrinters = append(envPrinters, dockerEnv) - p.injector.Register("dockerEnv", dockerEnv) - } - - if p.configHandler.GetBool("cluster.enabled", false) { - kubeEnv := envpkg.NewKubeEnvPrinter(p.injector) - envPrinters = append(envPrinters, kubeEnv) - p.injector.Register("kubeEnv", kubeEnv) - } - - clusterDriver := p.configHandler.GetString("cluster.driver", "") - if clusterDriver == "talos" || clusterDriver == "omni" { - talosEnv := envpkg.NewTalosEnvPrinter(p.injector) - envPrinters = append(envPrinters, talosEnv) - p.injector.Register("talosEnv", talosEnv) - } - - // Always register terraformEnv in the injector since the stack needs it - terraformEnv := envpkg.NewTerraformEnvPrinter(p.injector) - p.injector.Register("terraformEnv", terraformEnv) - - // Only include it in the returned array when terraform is enabled - if p.configHandler.GetBool("terraform.enabled", false) { - envPrinters = append(envPrinters, terraformEnv) - } - - windsorEnv := envpkg.NewWindsorEnvPrinter(p.injector) - envPrinters = append(envPrinters, windsorEnv) - p.injector.Register("windsorEnv", windsorEnv) - - return envPrinters, nil -} - -// withSecretsProviders creates secrets providers based on configuration -func (p *BasePipeline) withSecretsProviders() ([]secrets.SecretsProvider, error) { - if p.configHandler == nil { - return nil, fmt.Errorf("config handler not initialized") - } - - var secretsProviders []secrets.SecretsProvider - - configRoot, err := p.configHandler.GetConfigRoot() - if err != nil { - return nil, fmt.Errorf("error getting config root: %w", err) - } - - secretsFilePaths := []string{"secrets.enc.yaml", "secrets.enc.yml"} - for _, filePath := range secretsFilePaths { - if _, err := p.shims.Stat(filepath.Join(configRoot, filePath)); err == nil { - secretsProviders = append(secretsProviders, secrets.NewSopsSecretsProvider(configRoot, p.injector)) - break - } - } - - vaults, ok := p.configHandler.Get("secrets.onepassword.vaults").(map[string]secretsConfigType.OnePasswordVault) - if ok && len(vaults) > 0 { - useSDK := p.shims.Getenv("OP_SERVICE_ACCOUNT_TOKEN") != "" - - for key, vault := range vaults { - vaultCopy := vault - vaultCopy.ID = key - - if useSDK { - secretsProviders = append(secretsProviders, secrets.NewOnePasswordSDKSecretsProvider(vaultCopy, p.injector)) - } else { - secretsProviders = append(secretsProviders, secrets.NewOnePasswordCLISecretsProvider(vaultCopy, p.injector)) - } - } - } - - return secretsProviders, nil -} - -// withServices creates and configures service instances based on the current configuration. -// Services are created conditionally based on feature flags and configuration settings. -// Each service is registered in the DI container with appropriate naming conventions. -func (p *BasePipeline) withServices() ([]services.Service, error) { - if p.configHandler == nil { - return nil, fmt.Errorf("config handler not initialized") - } - - var serviceList []services.Service - - dockerEnabled := p.configHandler.GetBool("docker.enabled", false) - if !dockerEnabled { - return serviceList, nil - } - - dnsEnabled := p.configHandler.GetBool("dns.enabled", false) - if dnsEnabled { - service := services.NewDNSService(p.injector) - service.SetName("dns") - p.injector.Register("dnsService", service) - serviceList = append(serviceList, service) - } - - gitEnabled := p.configHandler.GetBool("git.livereload.enabled", false) - if gitEnabled { - service := services.NewGitLivereloadService(p.injector) - service.SetName("git") - p.injector.Register("gitLivereloadService", service) - serviceList = append(serviceList, service) - } - - awsEnabled := p.configHandler.GetBool("aws.localstack.enabled", false) - if awsEnabled { - service := services.NewLocalstackService(p.injector) - service.SetName("aws") - p.injector.Register("localstackService", service) - serviceList = append(serviceList, service) - } - - contextConfig := p.configHandler.GetConfig() - if contextConfig.Docker != nil && contextConfig.Docker.Registries != nil { - for key := range contextConfig.Docker.Registries { - service := services.NewRegistryService(p.injector) - service.SetName(key) - p.injector.Register(fmt.Sprintf("registryService.%s", key), service) - serviceList = append(serviceList, service) - } - } - - // Add cluster services based on cluster driver - clusterDriver := p.configHandler.GetString("cluster.driver", "") - switch clusterDriver { - case "talos", "omni": - controlPlaneCount := p.configHandler.GetInt("cluster.controlplanes.count") - workerCount := p.configHandler.GetInt("cluster.workers.count") - - for i := 1; i <= controlPlaneCount; i++ { - controlPlaneService := services.NewTalosService(p.injector, "controlplane") - serviceName := fmt.Sprintf("controlplane-%d", i) - controlPlaneService.SetName(serviceName) - p.injector.Register(fmt.Sprintf("clusterNode.%s", serviceName), controlPlaneService) - serviceList = append(serviceList, controlPlaneService) - } - - for i := 1; i <= workerCount; i++ { - workerService := services.NewTalosService(p.injector, "worker") - serviceName := fmt.Sprintf("worker-%d", i) - workerService.SetName(serviceName) - p.injector.Register(fmt.Sprintf("clusterNode.%s", serviceName), workerService) - serviceList = append(serviceList, workerService) - } - case "eks", "aks": - // For managed cloud clusters (EKS, AKS), no local cluster services are needed - // The cluster is managed by the cloud provider - break - } - - return serviceList, nil -} - -// withTerraformResolvers constructs and registers terraform module resolvers based on configuration state. -// If terraform.enabled is true in the configuration, the method instantiates both StandardModuleResolver and OCIModuleResolver, -// registers them in the dependency injection container, and returns them as a slice. If terraform is not enabled, returns an empty slice. -// Returns an error if the config handler is uninitialized. -func (p *BasePipeline) withTerraformResolvers() ([]terraform.ModuleResolver, error) { - if p.configHandler == nil { - return nil, fmt.Errorf("config handler not initialized") - } - - var resolvers []terraform.ModuleResolver - - if !p.configHandler.GetBool("terraform.enabled", false) { - return resolvers, nil - } - - standardResolver := terraform.NewStandardModuleResolver(p.injector) - p.injector.Register("standardModuleResolver", standardResolver) - resolvers = append(resolvers, standardResolver) - - ociResolver := terraform.NewOCIModuleResolver(p.injector) - p.injector.Register("ociModuleResolver", ociResolver) - resolvers = append(resolvers, ociResolver) - - return resolvers, nil -} - -// prepareTemplateData loads template data for pipeline execution. -// Source priority: blueprint context, local handler data, default artifact, -// then default template for current context. Returns a map of template file -// names to byte content, or error if loading fails. -func (p *BasePipeline) prepareTemplateData(ctx context.Context) (map[string][]byte, error) { - var blueprintValue string - if blueprintCtx := ctx.Value("blueprint"); blueprintCtx != nil { - if blueprint, ok := blueprintCtx.(string); ok { - blueprintValue = blueprint - } - } - - if blueprintValue != "" { - if p.artifactBuilder != nil { - ociInfo, err := artifact.ParseOCIReference(blueprintValue) - if err != nil { - return nil, fmt.Errorf("failed to parse blueprint reference: %w", err) - } - if ociInfo == nil { - return nil, fmt.Errorf("invalid blueprint reference: %s", blueprintValue) - } - templateData, err := p.artifactBuilder.GetTemplateData(ociInfo.URL) - if err != nil { - return nil, fmt.Errorf("failed to get template data from blueprint: %w", err) - } - return templateData, nil - } - } - - if p.blueprintHandler != nil { - blueprintTemplateData, err := p.blueprintHandler.GetLocalTemplateData() - if err != nil { - return nil, fmt.Errorf("failed to get local template data: %w", err) - } - - if len(blueprintTemplateData) > 0 { - return blueprintTemplateData, nil - } - } - - if p.artifactBuilder != nil { - effectiveBlueprintURL := constants.GetEffectiveBlueprintURL() - ociInfo, err := artifact.ParseOCIReference(effectiveBlueprintURL) - if err != nil { - return nil, fmt.Errorf("failed to parse default blueprint reference: %w", err) - } - templateData, err := p.artifactBuilder.GetTemplateData(ociInfo.URL) - if err != nil { - return nil, fmt.Errorf("failed to get template data from default blueprint: %w", err) - } - return templateData, nil - } - - if p.blueprintHandler != nil { - contextName := p.determineContextName(ctx) - defaultTemplateData, err := p.blueprintHandler.GetDefaultTemplateData(contextName) - if err != nil { - return nil, fmt.Errorf("failed to get default template data: %w", err) - } - return defaultTemplateData, nil - } - - return make(map[string][]byte), nil -} - -// determineContextName selects the context name from ctx, config, or defaults to "local" if unset or "local". -func (p *BasePipeline) determineContextName(ctx context.Context) string { - if contextName := ctx.Value("contextName"); contextName != nil { - if name, ok := contextName.(string); ok { - return name - } - } - if p.configHandler != nil { - currentContext := p.configHandler.GetContext() - if currentContext != "" && currentContext != "local" { - return currentContext - } - } - return "local" -} - -// ============================================================================= -// Interface Compliance -// ============================================================================= - -var _ Pipeline = (*BasePipeline)(nil) diff --git a/pkg/pipelines/pipeline_test.go b/pkg/pipelines/pipeline_test.go deleted file mode 100644 index bf95ea36b..000000000 --- a/pkg/pipelines/pipeline_test.go +++ /dev/null @@ -1,2893 +0,0 @@ -package pipelines - -import ( - "context" - "fmt" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/windsorcli/cli/api/v1alpha1" - "github.com/windsorcli/cli/api/v1alpha1/docker" - secretsConfigType "github.com/windsorcli/cli/api/v1alpha1/secrets" - "github.com/windsorcli/cli/pkg/context/config" - "github.com/windsorcli/cli/pkg/di" - envvars "github.com/windsorcli/cli/pkg/context/env" - "github.com/windsorcli/cli/pkg/context/tools" - "github.com/windsorcli/cli/pkg/provisioner/cluster" - "github.com/windsorcli/cli/pkg/provisioner/kubernetes" - k8sclient "github.com/windsorcli/cli/pkg/provisioner/kubernetes/client" - terraforminfra "github.com/windsorcli/cli/pkg/provisioner/terraform" - "github.com/windsorcli/cli/pkg/composer/artifact" - "github.com/windsorcli/cli/pkg/composer/blueprint" - "github.com/windsorcli/cli/pkg/context/shell" - "github.com/windsorcli/cli/pkg/workstation/virt" -) - -// ============================================================================= -// Centralized Mock Types -// ============================================================================= - -type mockInitFileInfo struct { - name string - isDir bool -} - -func (m *mockInitFileInfo) Name() string { return m.name } -func (m *mockInitFileInfo) Size() int64 { return 0 } -func (m *mockInitFileInfo) Mode() os.FileMode { return 0 } -func (m *mockInitFileInfo) ModTime() time.Time { return time.Time{} } -func (m *mockInitFileInfo) IsDir() bool { return m.isDir } -func (m *mockInitFileInfo) Sys() any { return nil } - -type mockInitDirEntry struct { - name string - isDir bool -} - -func (m *mockInitDirEntry) Name() string { return m.name } -func (m *mockInitDirEntry) IsDir() bool { return m.isDir } -func (m *mockInitDirEntry) Type() os.FileMode { return 0 } -func (m *mockInitDirEntry) Info() (os.FileInfo, error) { return nil, nil } - -// ============================================================================= -// Test Setup -// ============================================================================= - -type Mocks struct { - Injector di.Injector - ConfigHandler config.ConfigHandler - Shell *shell.MockShell - Shims *Shims -} - -type SetupOptions struct { - Injector di.Injector - ConfigHandler config.ConfigHandler - ConfigStr string -} - -func setupShims(t *testing.T) *Shims { - t.Helper() - shims := NewShims() - - shims.Stat = func(name string) (os.FileInfo, error) { - return nil, os.ErrNotExist // Default to file not found - } - - shims.Getenv = func(key string) string { - return "" // Default to empty string - } - - return shims -} - -func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { - t.Helper() - - // Store original directory and create temp dir - origDir, err := os.Getwd() - if err != nil { - t.Fatalf("Failed to get working directory: %v", err) - } - - tmpDir := t.TempDir() - if err := os.Chdir(tmpDir); err != nil { - t.Fatalf("Failed to change to temp directory: %v", err) - } - - // Set project root environment variable - t.Setenv("WINDSOR_PROJECT_ROOT", tmpDir) - - // Register cleanup to restore original state - t.Cleanup(func() { - os.Unsetenv("WINDSOR_PROJECT_ROOT") - if err := os.Chdir(origDir); err != nil { - t.Logf("Warning: Failed to change back to original directory: %v", err) - } - }) - - // Create injector if not provided - var injector di.Injector - if len(opts) > 0 && opts[0].Injector != nil { - injector = opts[0].Injector - } else { - injector = di.NewInjector() - } - - // Create and register mock shell - mockShell := shell.NewMockShell() - mockShell.InitializeFunc = func() error { return nil } - mockShell.GetProjectRootFunc = func() (string, error) { return tmpDir, nil } - injector.Register("shell", mockShell) - - // Create config handler if not provided - var configHandler config.ConfigHandler - if len(opts) > 0 && opts[0].ConfigHandler != nil { - configHandler = opts[0].ConfigHandler - } else { - configHandler = config.NewConfigHandler(injector) - } - injector.Register("configHandler", configHandler) - - // Initialize config handler - if err := configHandler.Initialize(); err != nil { - t.Fatalf("Failed to initialize config handler: %v", err) - } - - configHandler.SetContext("mock-context") - - // Load base config - configYAML := ` -apiVersion: v1alpha1 -contexts: - mock-context: - dns: - domain: mock.domain.com - network: - cidr_block: 10.0.0.0/24` - - if err := configHandler.LoadConfigString(configYAML); err != nil { - t.Fatalf("Failed to load config: %v", err) - } - - // Load optional config if provided - if len(opts) > 0 && opts[0].ConfigStr != "" { - if err := configHandler.LoadConfigString(opts[0].ConfigStr); err != nil { - t.Fatalf("Failed to load config string: %v", err) - } - } - - // Create context directory and config file to ensure loaded flag is set - contextDir := filepath.Join(tmpDir, "contexts", "mock-context") - if err := os.MkdirAll(contextDir, 0755); err != nil { - t.Fatalf("Failed to create context directory: %v", err) - } - contextConfigPath := filepath.Join(contextDir, "windsor.yaml") - contextConfigYAML := ` -dns: - domain: mock.domain.com -network: - cidr_block: 10.0.0.0/24` - if err := os.WriteFile(contextConfigPath, []byte(contextConfigYAML), 0644); err != nil { - t.Fatalf("Failed to write context config: %v", err) - } - - // Register shims - shims := setupShims(t) - injector.Register("shims", shims) - - // Create and register mock kubernetes manager for complex pipelines - mockKubernetesManager := kubernetes.NewMockKubernetesManager(nil) - injector.Register("kubernetesManager", mockKubernetesManager) - - // Create and register mock blueprint handler for stack dependency - mockBlueprintHandler := blueprint.NewMockBlueprintHandler(injector) - injector.Register("blueprintHandler", mockBlueprintHandler) - - // Create and register mock artifact builder for install pipeline dependency - mockArtifactBuilder := artifact.NewMockArtifact() - mockArtifactBuilder.InitializeFunc = func(injector di.Injector) error { return nil } - injector.Register("artifactBuilder", mockArtifactBuilder) - - // Create and register terraformEnv for stack dependency - terraformEnv := envvars.NewTerraformEnvPrinter(injector) - injector.Register("terraformEnv", terraformEnv) - - return &Mocks{ - Injector: injector, - ConfigHandler: configHandler, - Shell: mockShell, - Shims: shims, - } -} - -// ============================================================================= -// Test Constructor -// ============================================================================= - -func TestNewBasePipeline(t *testing.T) { - t.Run("CreatesWithDefaults", func(t *testing.T) { - pipeline := NewBasePipeline() - - if pipeline == nil { - t.Fatal("Expected pipeline to not be nil") - } - }) -} - -// ============================================================================= -// Test Public Methods -// ============================================================================= - -func TestBasePipeline_Initialize(t *testing.T) { - setup := func(t *testing.T) (*BasePipeline, *Mocks) { - pipeline := NewBasePipeline() - mocks := setupMocks(t) - return pipeline, mocks - } - - t.Run("InitializeReturnsNilByDefault", func(t *testing.T) { - // Given a base pipeline - pipeline, mocks := setup(t) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - }) - - t.Run("SetsWindsorContextWhenContextNameProvided", func(t *testing.T) { - // Given a base pipeline and mock config handler - pipeline, mocks := setup(t) - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.InitializeFunc = func() error { return nil } - - contextSetCalled := false - var capturedContextName string - mockConfigHandler.SetContextFunc = func(contextName string) error { - contextSetCalled = true - capturedContextName = contextName - return nil - } - - mocks.Injector.Register("configHandler", mockConfigHandler) - - // And a context with contextName specified - ctx := context.WithValue(context.Background(), "contextName", "test-context") - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - - // And SetContext should be called with the correct context name - if !contextSetCalled { - t.Error("Expected SetContext to be called") - } - if capturedContextName != "test-context" { - t.Errorf("Expected SetContext to be called with 'test-context', got %s", capturedContextName) - } - }) - - t.Run("DoesNotSetContextWhenContextNameIsEmpty", func(t *testing.T) { - // Given a base pipeline and mock config handler - pipeline, mocks := setup(t) - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.InitializeFunc = func() error { return nil } - - contextSetCalled := false - mockConfigHandler.SetContextFunc = func(contextName string) error { - contextSetCalled = true - return nil - } - - mocks.Injector.Register("configHandler", mockConfigHandler) - - // And a context with empty contextName - ctx := context.WithValue(context.Background(), "contextName", "") - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - - // And SetContext should not be called - if contextSetCalled { - t.Error("Expected SetContext not to be called for empty context name") - } - }) - - t.Run("DoesNotSetContextWhenContextNameNotProvided", func(t *testing.T) { - // Given a base pipeline and mock config handler - pipeline, mocks := setup(t) - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.InitializeFunc = func() error { return nil } - - contextSetCalled := false - mockConfigHandler.SetContextFunc = func(contextName string) error { - contextSetCalled = true - return nil - } - - mocks.Injector.Register("configHandler", mockConfigHandler) - - // When initializing the pipeline without contextName in context - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - - // And SetContext should not be called - if contextSetCalled { - t.Error("Expected SetContext not to be called when contextName not provided") - } - }) - - t.Run("ReturnsErrorWhenSetContextFails", func(t *testing.T) { - // Given a base pipeline and mock config handler that fails on SetContext - pipeline, mocks := setup(t) - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.InitializeFunc = func() error { return nil } - mockConfigHandler.SetContextFunc = func(contextName string) error { - return fmt.Errorf("failed to set context") - } - - mocks.Injector.Register("configHandler", mockConfigHandler) - - // And a context with contextName specified - ctx := context.WithValue(context.Background(), "contextName", "test-context") - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, ctx) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - expectedErr := "failed to set Windsor context: failed to set context" - if err.Error() != expectedErr { - t.Errorf("Expected error %q, got %q", expectedErr, err.Error()) - } - }) - - t.Run("HandlesDifferentContextNameTypes", func(t *testing.T) { - // Given a base pipeline and mock config handler - pipeline, mocks := setup(t) - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.InitializeFunc = func() error { return nil } - - contextSetCalled := false - mockConfigHandler.SetContextFunc = func(contextName string) error { - contextSetCalled = true - return nil - } - - mocks.Injector.Register("configHandler", mockConfigHandler) - - // And a context with non-string contextName - ctx := context.WithValue(context.Background(), "contextName", 123) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - - // And SetContext should not be called for non-string values - if contextSetCalled { - t.Error("Expected SetContext not to be called for non-string contextName") - } - }) - - t.Run("AddsTrustedDirectoryWhenTrustContextIsTrue", func(t *testing.T) { - // Given a base pipeline and mocks - pipeline, mocks := setup(t) - - trustFuncCalled := false - mocks.Shell.AddCurrentDirToTrustedFileFunc = func() error { - trustFuncCalled = true - return nil - } - - // And a context with trust set to true - ctx := context.WithValue(context.Background(), "trust", true) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - - // And AddCurrentDirToTrustedFile should be called - if !trustFuncCalled { - t.Error("Expected AddCurrentDirToTrustedFile to be called") - } - }) - - t.Run("DoesNotAddTrustedDirectoryWhenTrustContextIsFalse", func(t *testing.T) { - // Given a base pipeline and mocks - pipeline, mocks := setup(t) - - trustFuncCalled := false - mocks.Shell.AddCurrentDirToTrustedFileFunc = func() error { - trustFuncCalled = true - return nil - } - - // And a context with trust set to false - ctx := context.WithValue(context.Background(), "trust", false) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, ctx) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - - // And AddCurrentDirToTrustedFile should not be called - if trustFuncCalled { - t.Error("Expected AddCurrentDirToTrustedFile not to be called when trust is false") - } - }) - - t.Run("DoesNotAddTrustedDirectoryWhenTrustContextNotSet", func(t *testing.T) { - // Given a base pipeline and mocks - pipeline, mocks := setup(t) - - trustFuncCalled := false - mocks.Shell.AddCurrentDirToTrustedFileFunc = func() error { - trustFuncCalled = true - return nil - } - - // When initializing the pipeline without trust context - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - - // And AddCurrentDirToTrustedFile should not be called - if trustFuncCalled { - t.Error("Expected AddCurrentDirToTrustedFile not to be called when trust context not set") - } - }) - - t.Run("ReturnsErrorWhenAddCurrentDirToTrustedFileFails", func(t *testing.T) { - // Given a base pipeline and mocks - pipeline, mocks := setup(t) - - mocks.Shell.AddCurrentDirToTrustedFileFunc = func() error { - return fmt.Errorf("failed to add trusted directory") - } - - // And a context with trust set to true - ctx := context.WithValue(context.Background(), "trust", true) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, ctx) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - expectedErr := "failed to add current directory to trusted file: failed to add trusted directory" - if err.Error() != expectedErr { - t.Errorf("Expected error %q, got %q", expectedErr, err.Error()) - } - }) -} - -func TestBasePipeline_Execute(t *testing.T) { - setup := func(t *testing.T) (*BasePipeline, *Mocks) { - pipeline := NewBasePipeline() - mocks := setupMocks(t) - return pipeline, mocks - } - - t.Run("ExecuteReturnsNilByDefault", func(t *testing.T) { - // Given a base pipeline - pipeline, _ := setup(t) - - // When executing the pipeline - err := pipeline.Execute(context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - }) -} - -func TestWithPipeline(t *testing.T) { - t.Run("Success", func(t *testing.T) { - testCases := []struct { - name string - pipelineType string - }{ - {"InitPipeline", "initPipeline"}, - {"ExecPipeline", "execPipeline"}, - {"CheckPipeline", "checkPipeline"}, - {"UpPipeline", "upPipeline"}, - {"DownPipeline", "downPipeline"}, - {"InstallPipeline", "installPipeline"}, - {"BasePipeline", "basePipeline"}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Given an injector with proper mocks - mocks := setupMocks(t) - - // When creating a pipeline with NewPipeline (without initialization) - pipeline, err := WithPipeline(mocks.Injector, context.Background(), tc.pipelineType) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - - // And a pipeline should be returned - if pipeline == nil { - t.Error("Expected non-nil pipeline") - } - }) - } - }) - - t.Run("UnknownPipelineType", func(t *testing.T) { - // Given an injector with proper mocks - mocks := setupMocks(t) - - // When creating a pipeline with unknown type - pipeline, err := WithPipeline(mocks.Injector, context.Background(), "unknownPipeline") - - // Then an error should be returned - if err == nil { - t.Error("Expected error for unknown pipeline type, got nil") - } - if !strings.Contains(err.Error(), "unknown pipeline") { - t.Errorf("Expected 'unknown pipeline' error, got: %v", err) - } - - // And no pipeline should be returned - if pipeline != nil { - t.Error("Expected nil pipeline for unknown type") - } - }) - - t.Run("WithPipelineInitialization", func(t *testing.T) { - // Given an injector with proper mocks - mocks := setupMocks(t) - - // When creating a pipeline with WithPipeline - pipeline, err := WithPipeline(mocks.Injector, context.Background(), "basePipeline") - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - - // And a pipeline should be returned - if pipeline == nil { - t.Error("Expected non-nil pipeline") - } - - // And the pipeline should be registered in the injector - registered := mocks.Injector.Resolve("basePipeline") - if registered == nil { - t.Error("Expected pipeline to be registered in injector") - } - }) - - t.Run("ExistingPipelineInInjector", func(t *testing.T) { - // Given an injector with existing pipeline - injector := di.NewInjector() - existingPipeline := NewMockBasePipeline() - injector.Register("initPipeline", existingPipeline) - ctx := context.Background() - - // When creating a pipeline with WithPipeline - pipeline, err := WithPipeline(injector, ctx, "initPipeline") - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - - // And the existing pipeline should be returned - if pipeline != existingPipeline { - t.Error("Expected existing pipeline to be returned") - } - }) - - t.Run("ExistingNonPipelineInInjector", func(t *testing.T) { - // Given an injector with non-pipeline object - injector := di.NewInjector() - nonPipeline := "not a pipeline" - injector.Register("initPipeline", nonPipeline) - ctx := context.Background() - - // When creating a pipeline with WithPipeline - pipeline, err := WithPipeline(injector, ctx, "initPipeline") - - // Then no error should be returned (it creates a new pipeline) - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - - // And a new pipeline should be returned - if pipeline == nil { - t.Error("Expected non-nil pipeline") - } - }) - - t.Run("ContextPropagation", func(t *testing.T) { - // Given an injector and context with values - injector := di.NewInjector() - ctx := context.WithValue(context.Background(), "testKey", "testValue") - - // When creating a pipeline with WithPipeline - pipeline, err := WithPipeline(injector, ctx, "initPipeline") - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - - // And pipeline should be created successfully - if pipeline == nil { - t.Error("Expected non-nil pipeline") - } - }) - - t.Run("NilInjector", func(t *testing.T) { - // Given a nil injector - ctx := context.Background() - - // When creating a pipeline with nil injector, it should panic - defer func() { - if r := recover(); r == nil { - t.Error("Expected panic for nil injector, but no panic occurred") - } - }() - - // This should panic due to nil pointer dereference - WithPipeline(nil, ctx, "initPipeline") - - // If we reach here, the test should fail - t.Error("Expected panic for nil injector, but function returned normally") - }) - - t.Run("NilContext", func(t *testing.T) { - // Given an injector with nil context - injector := di.NewInjector() - - // When creating a pipeline with nil context, it should panic - defer func() { - if r := recover(); r == nil { - t.Error("Expected panic for nil context, but no panic occurred") - } - }() - - // This should panic due to nil pointer dereference during initialization - WithPipeline(injector, nil, "initPipeline") - - // If we reach here, the test should fail - t.Error("Expected panic for nil context, but function returned normally") - }) - - t.Run("EmptyPipelineType", func(t *testing.T) { - // Given an injector and context - injector := di.NewInjector() - ctx := context.Background() - - // When creating a pipeline with empty type - pipeline, err := WithPipeline(injector, ctx, "") - - // Then an error should be returned - if err == nil { - t.Error("Expected error for empty pipeline type, got nil") - } - if !strings.Contains(err.Error(), "unknown pipeline") { - t.Errorf("Expected 'unknown pipeline' error, got: %v", err) - } - - // And no pipeline should be returned - if pipeline != nil { - t.Error("Expected nil pipeline for empty type") - } - }) - - t.Run("AllSupportedTypes", func(t *testing.T) { - supportedTypes := []string{ - "initPipeline", - "execPipeline", - "checkPipeline", - "basePipeline", - } - - for _, pipelineType := range supportedTypes { - t.Run(pipelineType, func(t *testing.T) { - // Given an injector and context - injector := di.NewInjector() - ctx := context.Background() - - // Set up required dependencies for check pipeline - if pipelineType == "checkPipeline" { - // Set up tools manager - mockToolsManager := tools.NewMockToolsManager() - mockToolsManager.InitializeFunc = func() error { return nil } - mockToolsManager.CheckFunc = func() error { return nil } - injector.Register("toolsManager", mockToolsManager) - - // Set up cluster client - mockClusterClient := cluster.NewMockClusterClient() - mockClusterClient.WaitForNodesHealthyFunc = func(ctx context.Context, nodeAddresses []string, expectedVersion string) error { - return nil - } - injector.Register("clusterClient", mockClusterClient) - - // Set up kubernetes manager - mockKubernetesManager := kubernetes.NewMockKubernetesManager(injector) - mockKubernetesManager.InitializeFunc = func() error { return nil } - mockKubernetesManager.WaitForKubernetesHealthyFunc = func(ctx context.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { - return nil - } - injector.Register("kubernetesManager", mockKubernetesManager) - } - - // When creating the pipeline - pipeline, err := WithPipeline(injector, ctx, pipelineType) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error for %s, got: %v", pipelineType, err) - } - - // And a pipeline should be returned - if pipeline == nil { - t.Errorf("Expected non-nil pipeline for %s", pipelineType) - } - }) - } - - // Special test for upPipeline which requires more complex setup - t.Run("upPipeline", func(t *testing.T) { - // Given an injector with proper mocks for up pipeline - mocks := setupMocks(t) - - // When creating the up pipeline - pipeline, err := WithPipeline(mocks.Injector, context.Background(), "upPipeline") - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error for upPipeline, got: %v", err) - } - - // And a pipeline should be returned - if pipeline == nil { - t.Error("Expected non-nil pipeline for upPipeline") - } - }) - }) - - t.Run("PipelineRegistration", func(t *testing.T) { - // Given an injector and context - injector := di.NewInjector() - ctx := context.Background() - - // When creating a pipeline with WithPipeline - pipeline, err := WithPipeline(injector, ctx, "initPipeline") - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - - // And the pipeline should be registered in the injector - registered := injector.Resolve("initPipeline") - if registered == nil { - t.Error("Expected pipeline to be registered in injector") - } - if registered != pipeline { - t.Error("Expected registered pipeline to match returned pipeline") - } - }) - - t.Run("FactoryFunctionality", func(t *testing.T) { - // Given an injector and context - injector := di.NewInjector() - ctx := context.Background() - - // When creating multiple pipelines of the same type - pipeline1, err1 := WithPipeline(injector, ctx, "initPipeline") - pipeline2, err2 := WithPipeline(injector, ctx, "initPipeline") - - // Then both calls should succeed - if err1 != nil { - t.Errorf("Expected no error for first call, got: %v", err1) - } - if err2 != nil { - t.Errorf("Expected no error for second call, got: %v", err2) - } - - // And the same pipeline instance should be returned - if pipeline1 != pipeline2 { - t.Error("Expected same pipeline instance to be returned from factory") - } - }) -} - -// ============================================================================= -// Test Protected Methods -// ============================================================================= - -func TestBasePipeline_handleSessionReset(t *testing.T) { - setup := func(t *testing.T) (*BasePipeline, *shell.MockShell) { - t.Helper() - pipeline := NewBasePipeline() - mockShell := shell.NewMockShell() - pipeline.shell = mockShell - - // Clean up any existing environment variables - t.Cleanup(func() { - os.Unsetenv("WINDSOR_SESSION_TOKEN") - os.Unsetenv("NO_CACHE") - }) - - return pipeline, mockShell - } - - t.Run("ReturnsNilWhenShellIsNil", func(t *testing.T) { - // Given a pipeline with nil shell - pipeline := &BasePipeline{} - - // When handling session reset - err := pipeline.handleSessionReset() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("ResetsWhenNoSessionToken", func(t *testing.T) { - // Given a pipeline with no session token - pipeline, mockShell := setup(t) - - // Ensure no session token is set - os.Unsetenv("WINDSOR_SESSION_TOKEN") - - mockShell.CheckResetFlagsFunc = func() (bool, error) { - return false, nil - } - resetCalled := false - mockShell.ResetFunc = func(...bool) { - resetCalled = true - } - - // When handling session reset - err := pipeline.handleSessionReset() - - // Then reset should be called - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if !resetCalled { - t.Error("Expected shell reset to be called") - } - }) - - t.Run("ResetsWhenResetFlagsTrue", func(t *testing.T) { - // Given a pipeline with reset flags true - pipeline, mockShell := setup(t) - - // Set a session token - os.Setenv("WINDSOR_SESSION_TOKEN", "test-token") - - mockShell.CheckResetFlagsFunc = func() (bool, error) { - return true, nil - } - resetCalled := false - mockShell.ResetFunc = func(...bool) { - resetCalled = true - } - - // When handling session reset - err := pipeline.handleSessionReset() - - // Then reset should be called - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if !resetCalled { - t.Error("Expected shell reset to be called") - } - }) - - t.Run("DoesNotResetWhenSessionTokenExistsAndResetFlagsFalse", func(t *testing.T) { - // Given a pipeline with session token and reset flags false - pipeline, mockShell := setup(t) - - // Set a session token - os.Setenv("WINDSOR_SESSION_TOKEN", "test-token") - - mockShell.CheckResetFlagsFunc = func() (bool, error) { - return false, nil - } - resetCalled := false - mockShell.ResetFunc = func(...bool) { - resetCalled = true - } - - // When handling session reset - err := pipeline.handleSessionReset() - - // Then reset should not be called - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if resetCalled { - t.Error("Expected shell reset to not be called") - } - }) - - t.Run("ReturnsErrorWhenCheckResetFlagsFails", func(t *testing.T) { - // Given a pipeline where check reset flags fails - pipeline, mockShell := setup(t) - - // Ensure no session token is set - os.Unsetenv("WINDSOR_SESSION_TOKEN") - - mockShell.CheckResetFlagsFunc = func() (bool, error) { - return false, fmt.Errorf("check reset flags error") - } - - // When handling session reset - err := pipeline.handleSessionReset() - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "check reset flags error" { - t.Errorf("Expected check reset flags error, got: %v", err) - } - }) - - t.Run("HandlesSessionResetWithNoSessionToken", func(t *testing.T) { - // Given a base pipeline with no session token - pipeline := NewBasePipeline() - - mockShell := shell.NewMockShell() - mockShell.CheckResetFlagsFunc = func() (bool, error) { - return false, nil - } - mockShell.ResetFunc = func(args ...bool) { - // Reset called - } - pipeline.shell = mockShell - - // Ensure no session token is set - originalToken := os.Getenv("WINDSOR_SESSION_TOKEN") - os.Unsetenv("WINDSOR_SESSION_TOKEN") - defer func() { - if originalToken != "" { - os.Setenv("WINDSOR_SESSION_TOKEN", originalToken) - } - }() - - // When handling session reset - err := pipeline.handleSessionReset() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("HandlesSessionResetWithSessionToken", func(t *testing.T) { - // Given a base pipeline with session token - pipeline := NewBasePipeline() - - mockShell := shell.NewMockShell() - mockShell.CheckResetFlagsFunc = func() (bool, error) { - return false, nil - } - pipeline.shell = mockShell - - // Set session token - originalToken := os.Getenv("WINDSOR_SESSION_TOKEN") - os.Setenv("WINDSOR_SESSION_TOKEN", "test-token") - defer func() { - if originalToken != "" { - os.Setenv("WINDSOR_SESSION_TOKEN", originalToken) - } else { - os.Unsetenv("WINDSOR_SESSION_TOKEN") - } - }() - - // When handling session reset - err := pipeline.handleSessionReset() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("HandlesSessionResetWithNilShell", func(t *testing.T) { - // Given a base pipeline with nil shell - pipeline := NewBasePipeline() - pipeline.shell = nil - - // When handling session reset - err := pipeline.handleSessionReset() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) -} - -// ============================================================================= -// Test Private Methods -// ============================================================================= - -func TestBasePipeline_withEnvPrinters(t *testing.T) { - t.Run("CreatesWindsorEnvPrinterByDefault", func(t *testing.T) { - // Given a base pipeline with minimal configuration - pipeline := NewBasePipeline() - - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - return false - } - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - return "" - } - pipeline.configHandler = mockConfigHandler - pipeline.injector = di.NewInjector() - - // When creating env printers - envPrinters, err := pipeline.withEnvPrinters() - - // Then no error should be returned and Windsor env printer should be created - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(envPrinters) != 1 { - t.Errorf("Expected 1 env printer, got %d", len(envPrinters)) - } - }) - - t.Run("CreatesMultipleEnvPrintersWhenEnabled", func(t *testing.T) { - // Given a base pipeline with multiple services enabled - pipeline := NewBasePipeline() - - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - switch key { - case "aws.enabled": - return true - case "azure.enabled": - return true - case "docker.enabled": - return true - case "cluster.enabled": - return true - case "terraform.enabled": - return true - default: - return false - } - } - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - switch key { - case "cluster.driver": - return "talos" - default: - return "" - } - } - pipeline.configHandler = mockConfigHandler - pipeline.injector = di.NewInjector() - - // When creating env printers - envPrinters, err := pipeline.withEnvPrinters() - - // Then no error should be returned and multiple env printers should be created - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - // Should have AWS, Azure, Docker, Kube, Talos, Terraform, and Windsor - if len(envPrinters) != 7 { - t.Errorf("Expected 7 env printers, got %d", len(envPrinters)) - } - }) - - t.Run("CreatesTalosEnvPrinterWhenOmniProvider", func(t *testing.T) { - // Given a base pipeline with omni cluster provider - pipeline := NewBasePipeline() - - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - return false - } - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - switch key { - case "cluster.driver": - return "omni" - default: - return "" - } - } - pipeline.configHandler = mockConfigHandler - pipeline.injector = di.NewInjector() - - // When creating env printers - envPrinters, err := pipeline.withEnvPrinters() - - // Then no error should be returned and Talos and Windsor env printers should be created - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - // Should have Talos and Windsor - if len(envPrinters) != 2 { - t.Errorf("Expected 2 env printers, got %d", len(envPrinters)) - } - }) - - t.Run("ReturnsErrorWhenConfigHandlerIsNil", func(t *testing.T) { - // Given a base pipeline with nil config handler - pipeline := NewBasePipeline() - pipeline.configHandler = nil - - // When creating env printers - envPrinters, err := pipeline.withEnvPrinters() - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "config handler not initialized" { - t.Errorf("Expected 'config handler not initialized', got: %v", err) - } - if envPrinters != nil { - t.Error("Expected nil env printers") - } - }) - - t.Run("RegistersAllEnvPrintersInDIContainer", func(t *testing.T) { - // Given a base pipeline with all services enabled - pipeline := NewBasePipeline() - - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - switch key { - case "aws.enabled": - return true - case "azure.enabled": - return true - case "docker.enabled": - return true - case "cluster.enabled": - return true - case "terraform.enabled": - return true - default: - if len(defaultValue) > 0 { - return defaultValue[0] - } - return false - } - } - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - switch key { - case "cluster.driver": - return "talos" - default: - if len(defaultValue) > 0 { - return defaultValue[0] - } - return "" - } - } - pipeline.configHandler = mockConfigHandler - pipeline.injector = di.NewInjector() - - // When creating env printers - envPrinters, err := pipeline.withEnvPrinters() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And all expected env printers should be registered in the DI container - expectedRegistrations := []string{ - "awsEnv", - "azureEnv", - "dockerEnv", - "kubeEnv", - "talosEnv", - "terraformEnv", // Always registered even if not in returned slice - "windsorEnv", - } - - for _, expectedKey := range expectedRegistrations { - resolved := pipeline.injector.Resolve(expectedKey) - if resolved == nil { - t.Errorf("Expected %s to be registered in DI container, but it was not found", expectedKey) - } - } - - // And the correct number of env printers should be returned - // Should have AWS, Azure, Docker, Kube, Talos, Terraform, and Windsor - if len(envPrinters) != 7 { - t.Errorf("Expected 7 env printers, got %d", len(envPrinters)) - } - }) - - t.Run("RegistersTalosEnvPrinterInDIContainer", func(t *testing.T) { - // Given a base pipeline with omni cluster provider - pipeline := NewBasePipeline() - - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - if len(defaultValue) > 0 { - return defaultValue[0] - } - return false - } - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - switch key { - case "cluster.driver": - return "omni" - default: - if len(defaultValue) > 0 { - return defaultValue[0] - } - return "" - } - } - pipeline.configHandler = mockConfigHandler - pipeline.injector = di.NewInjector() - - // When creating env printers - envPrinters, err := pipeline.withEnvPrinters() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And talos, terraform, and windsor env printers should be registered - expectedRegistrations := []string{ - "talosEnv", - "terraformEnv", // Always registered - "windsorEnv", - } - - for _, expectedKey := range expectedRegistrations { - resolved := pipeline.injector.Resolve(expectedKey) - if resolved == nil { - t.Errorf("Expected %s to be registered in DI container, but it was not found", expectedKey) - } - } - - // And the correct number of env printers should be returned - // Should have Talos and Windsor (terraform not included in slice when disabled) - if len(envPrinters) != 2 { - t.Errorf("Expected 2 env printers, got %d", len(envPrinters)) - } - }) - - t.Run("AlwaysRegistersTerraformEnvEvenWhenDisabled", func(t *testing.T) { - // Given a base pipeline with terraform disabled - pipeline := NewBasePipeline() - - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - switch key { - case "terraform.enabled": - return false // Explicitly disabled - default: - if len(defaultValue) > 0 { - return defaultValue[0] - } - return false - } - } - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - if len(defaultValue) > 0 { - return defaultValue[0] - } - return "" - } - pipeline.configHandler = mockConfigHandler - pipeline.injector = di.NewInjector() - - // When creating env printers - envPrinters, err := pipeline.withEnvPrinters() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And terraform env should still be registered in DI container - terraformEnv := pipeline.injector.Resolve("terraformEnv") - if terraformEnv == nil { - t.Error("Expected terraformEnv to be registered in DI container even when disabled") - } - - // But terraform should not be included in the returned slice - // Should only have Windsor - if len(envPrinters) != 1 { - t.Errorf("Expected 1 env printer, got %d", len(envPrinters)) - } - }) -} - -func TestBasePipeline_withSecretsProviders(t *testing.T) { - setup := func(t *testing.T) (*BasePipeline, *Mocks, string) { - pipeline := NewBasePipeline() - - // Create temp directory for secrets files - tmpDir := t.TempDir() - - // Create mock config handler - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetConfigRootFunc = func() (string, error) { - return tmpDir, nil - } - mockConfigHandler.GetContextFunc = func() string { - return "test-context" - } - mockConfigHandler.GetFunc = func(key string) any { - return nil - } - - // Create setup options with mock config handler - opts := &SetupOptions{ - ConfigHandler: mockConfigHandler, - } - mocks := setupMocks(t, opts) - - pipeline.configHandler = mockConfigHandler - pipeline.shims = mocks.Shims - pipeline.injector = mocks.Injector - return pipeline, mocks, tmpDir - } - - t.Run("ReturnsEmptyWhenNoSecretsConfigured", func(t *testing.T) { - // Given a base pipeline with no secrets configuration - pipeline, _, _ := setup(t) - - // When creating secrets providers - secretsProviders, err := pipeline.withSecretsProviders() - - // Then no error should be returned and no providers should be created - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(secretsProviders) != 0 { - t.Errorf("Expected 0 secrets providers, got %d", len(secretsProviders)) - } - }) - - t.Run("CreatesSopsProviderWhenSecretsFileExists", func(t *testing.T) { - // Given a base pipeline with secrets file - pipeline, mocks, tmpDir := setup(t) - - // Create secrets file - secretsFile := filepath.Join(tmpDir, "secrets.enc.yaml") - if err := os.WriteFile(secretsFile, []byte("test"), 0644); err != nil { - t.Fatalf("Failed to create secrets file: %v", err) - } - - // Configure shims to return file exists for secrets file - mocks.Shims.Stat = func(name string) (os.FileInfo, error) { - if name == secretsFile { - return os.Stat(secretsFile) // Return actual file info - } - return nil, os.ErrNotExist - } - - // When creating secrets providers - secretsProviders, err := pipeline.withSecretsProviders() - - // Then no error should be returned and SOPS provider should be created - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(secretsProviders) != 1 { - t.Errorf("Expected 1 secrets provider, got %d", len(secretsProviders)) - } - }) - - t.Run("ReturnsErrorWhenConfigHandlerIsNil", func(t *testing.T) { - // Given a base pipeline with nil config handler - pipeline, _, _ := setup(t) - pipeline.configHandler = nil - - // When creating secrets providers - secretsProviders, err := pipeline.withSecretsProviders() - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "config handler not initialized" { - t.Errorf("Expected 'config handler not initialized', got: %v", err) - } - if secretsProviders != nil { - t.Error("Expected nil secrets providers") - } - }) - - t.Run("ReturnsErrorWhenGetConfigRootFails", func(t *testing.T) { - // Given a base pipeline with failing config root - pipeline, mocks, _ := setup(t) - - // Configure mock to fail on GetConfigRoot - mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) - mockConfigHandler.GetConfigRootFunc = func() (string, error) { - return "", fmt.Errorf("config root error") - } - - // When creating secrets providers - secretsProviders, err := pipeline.withSecretsProviders() - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if !strings.Contains(err.Error(), "error getting config root") { - t.Errorf("Expected 'error getting config root' in error, got: %v", err) - } - if secretsProviders != nil { - t.Error("Expected nil secrets providers") - } - }) - - t.Run("CreatesOnePasswordSDKProviderWhenServiceAccountTokenSet", func(t *testing.T) { - // Given a base pipeline with OnePassword vaults and service account token - pipeline, mocks, _ := setup(t) - - // Configure OnePassword vaults - mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) - mockConfigHandler.GetFunc = func(key string) any { - if key == "secrets.onepassword.vaults" { - return map[string]secretsConfigType.OnePasswordVault{ - "vault1": { - Name: "test-vault", - }, - } - } - return nil - } - - // Configure service account token - mocks.Shims.Getenv = func(key string) string { - if key == "OP_SERVICE_ACCOUNT_TOKEN" { - return "test-token" - } - return "" - } - - // When creating secrets providers - secretsProviders, err := pipeline.withSecretsProviders() - - // Then no error should be returned and OnePassword SDK provider should be created - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(secretsProviders) != 1 { - t.Errorf("Expected 1 secrets provider, got %d", len(secretsProviders)) - } - }) - - t.Run("CreatesOnePasswordCLIProviderWhenNoServiceAccountToken", func(t *testing.T) { - // Given a base pipeline with OnePassword vaults and no service account token - pipeline, mocks, _ := setup(t) - - // Configure OnePassword vaults - mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) - mockConfigHandler.GetFunc = func(key string) any { - if key == "secrets.onepassword.vaults" { - return map[string]secretsConfigType.OnePasswordVault{ - "vault1": { - Name: "test-vault", - }, - } - } - return nil - } - - // Configure no service account token (already set to "" in setup) - - // When creating secrets providers - secretsProviders, err := pipeline.withSecretsProviders() - - // Then no error should be returned and OnePassword CLI provider should be created - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(secretsProviders) != 1 { - t.Errorf("Expected 1 secrets provider, got %d", len(secretsProviders)) - } - }) - - t.Run("CreatesSecretsProviderForSecretsDotEncDotYmlFile", func(t *testing.T) { - // Given a base pipeline with secrets.enc.yml file - pipeline, mocks, tmpDir := setup(t) - - // Create secrets.enc.yml file - secretsFile := filepath.Join(tmpDir, "secrets.enc.yml") - if err := os.WriteFile(secretsFile, []byte("test"), 0644); err != nil { - t.Fatalf("Failed to create secrets file: %v", err) - } - - // Configure shims to return file exists for secrets file - mocks.Shims.Stat = func(name string) (os.FileInfo, error) { - if name == secretsFile { - return os.Stat(secretsFile) // Return actual file info - } - return nil, os.ErrNotExist - } - - // When creating secrets providers - secretsProviders, err := pipeline.withSecretsProviders() - - // Then no error should be returned and SOPS provider should be created - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(secretsProviders) != 1 { - t.Errorf("Expected 1 secrets provider, got %d", len(secretsProviders)) - } - }) -} - -func TestBasePipeline_withBlueprintHandler(t *testing.T) { - setup := func(t *testing.T) (*BasePipeline, *Mocks) { - pipeline := NewBasePipeline() - mocks := setupMocks(t) - pipeline.injector = mocks.Injector - return pipeline, mocks - } - - t.Run("CreatesNewBlueprintHandlerWhenNotRegistered", func(t *testing.T) { - // Given a pipeline without blueprint handler - pipeline, _ := setup(t) - - // When getting blueprint handler - handler := pipeline.withBlueprintHandler() - - // Then a new handler should be created and registered - if handler == nil { - t.Error("Expected blueprint handler to be created") - } - - registered := pipeline.injector.Resolve("blueprintHandler") - if registered == nil { - t.Error("Expected blueprint handler to be registered") - } - }) - - t.Run("ReusesExistingBlueprintHandlerWhenRegistered", func(t *testing.T) { - // Given a pipeline with existing blueprint handler - pipeline, mocks := setup(t) - existingHandler := blueprint.NewBlueprintHandler(mocks.Injector) - pipeline.injector.Register("blueprintHandler", existingHandler) - - // When getting blueprint handler - handler := pipeline.withBlueprintHandler() - - // Then the existing handler should be returned - if handler != existingHandler { - t.Error("Expected existing blueprint handler to be reused") - } - }) - - t.Run("CreatesNewHandlerWhenRegisteredValueIsNotBlueprintHandler", func(t *testing.T) { - // Given a pipeline with wrong type registered - pipeline, _ := setup(t) - pipeline.injector.Register("blueprintHandler", "not-a-handler") - - // When getting blueprint handler - handler := pipeline.withBlueprintHandler() - - // Then a new handler should be created - if handler == nil { - t.Error("Expected blueprint handler to be created") - } - }) -} - -func TestBasePipeline_withStack(t *testing.T) { - setup := func(t *testing.T) (*BasePipeline, *Mocks) { - pipeline := NewBasePipeline() - mocks := setupMocks(t) - pipeline.injector = mocks.Injector - return pipeline, mocks - } - - t.Run("CreatesNewStackWhenNotRegistered", func(t *testing.T) { - // Given a pipeline without stack - pipeline, _ := setup(t) - - // When getting stack - stackInstance := pipeline.withStack() - - // Then a new stack should be created and registered - if stackInstance == nil { - t.Error("Expected stack to be created") - } - - registered := pipeline.injector.Resolve("stack") - if registered == nil { - t.Error("Expected stack to be registered") - } - }) - - t.Run("ReusesExistingStackWhenRegistered", func(t *testing.T) { - // Given a pipeline with existing stack - pipeline, mocks := setup(t) - existingStack := terraforminfra.NewWindsorStack(mocks.Injector) - pipeline.injector.Register("stack", existingStack) - - // When getting stack - stackInstance := pipeline.withStack() - - // Then the existing stack should be returned - if stackInstance != existingStack { - t.Error("Expected existing stack to be reused") - } - }) - - t.Run("CreatesNewStackWhenRegisteredValueIsNotStack", func(t *testing.T) { - // Given a pipeline with wrong type registered - pipeline, _ := setup(t) - pipeline.injector.Register("stack", "not-a-stack") - - // When getting stack - stackInstance := pipeline.withStack() - - // Then a new stack should be created - if stackInstance == nil { - t.Error("Expected stack to be created") - } - }) -} - -func TestBasePipeline_withArtifactBuilder(t *testing.T) { - setup := func(t *testing.T) (*BasePipeline, *Mocks) { - pipeline := NewBasePipeline() - mocks := setupMocks(t) - pipeline.injector = mocks.Injector - return pipeline, mocks - } - - t.Run("CreatesNewArtifactBuilderWhenNotRegistered", func(t *testing.T) { - // Given a pipeline without artifact builder - pipeline, _ := setup(t) - - // When getting artifact builder - builder := pipeline.withArtifactBuilder() - - // Then a new builder should be created and registered - if builder == nil { - t.Error("Expected artifact builder to be created") - } - - registered := pipeline.injector.Resolve("artifactBuilder") - if registered == nil { - t.Error("Expected artifact builder to be registered") - } - }) - - t.Run("ReusesExistingArtifactBuilderWhenRegistered", func(t *testing.T) { - // Given a pipeline with existing artifact builder - pipeline, _ := setup(t) - existingBuilder := artifact.NewArtifactBuilder() - pipeline.injector.Register("artifactBuilder", existingBuilder) - - // When getting artifact builder - builder := pipeline.withArtifactBuilder() - - // Then the existing builder should be returned - if builder != existingBuilder { - t.Error("Expected existing artifact builder to be reused") - } - }) - - t.Run("CreatesNewBuilderWhenRegisteredValueIsNotArtifactBuilder", func(t *testing.T) { - // Given a pipeline with wrong type registered - pipeline, _ := setup(t) - pipeline.injector.Register("artifactBuilder", "not-a-builder") - - // When getting artifact builder - builder := pipeline.withArtifactBuilder() - - // Then a new builder should be created - if builder == nil { - t.Error("Expected artifact builder to be created") - } - }) -} - -func TestBasePipeline_withVirtualMachine(t *testing.T) { - setup := func(t *testing.T) (*BasePipeline, *Mocks) { - pipeline := NewBasePipeline() - - // Create mock config handler - mockConfigHandler := config.NewMockConfigHandler() - opts := &SetupOptions{ - ConfigHandler: mockConfigHandler, - } - mocks := setupMocks(t, opts) - - pipeline.injector = mocks.Injector - pipeline.configHandler = mockConfigHandler - return pipeline, mocks - } - - t.Run("ReturnsNilWhenNoVMDriverConfigured", func(t *testing.T) { - // Given a pipeline with no VM driver configured - pipeline, _ := setup(t) - mockConfigHandler := pipeline.configHandler.(*config.MockConfigHandler) - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - if key == "vm.driver" { - return "" - } - return "" - } - - // When getting virtual machine - vm := pipeline.withVirtualMachine() - - // Then nil should be returned - if vm != nil { - t.Error("Expected nil virtual machine when no driver configured") - } - }) - - t.Run("CreatesColimaVMWhenColimaDriverConfigured", func(t *testing.T) { - // Given a pipeline with colima driver configured - pipeline, _ := setup(t) - mockConfigHandler := pipeline.configHandler.(*config.MockConfigHandler) - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - if key == "vm.driver" { - return "colima" - } - return "" - } - - // When getting virtual machine - vm := pipeline.withVirtualMachine() - - // Then colima VM should be created and registered - if vm == nil { - t.Error("Expected colima virtual machine to be created") - } - - registered := pipeline.injector.Resolve("virtualMachine") - if registered == nil { - t.Error("Expected virtual machine to be registered") - } - }) - - t.Run("ReturnsNilWhenUnsupportedVMDriverConfigured", func(t *testing.T) { - // Given a pipeline with unsupported VM driver configured - pipeline, _ := setup(t) - mockConfigHandler := pipeline.configHandler.(*config.MockConfigHandler) - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - if key == "vm.driver" { - return "unsupported" - } - return "" - } - - // When getting virtual machine - vm := pipeline.withVirtualMachine() - - // Then nil should be returned - if vm != nil { - t.Error("Expected nil virtual machine for unsupported driver") - } - }) - - t.Run("ReusesExistingVMWhenRegistered", func(t *testing.T) { - // Given a pipeline with existing VM - pipeline, mocks := setup(t) - mockConfigHandler := pipeline.configHandler.(*config.MockConfigHandler) - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - if key == "vm.driver" { - return "colima" - } - return "" - } - - existingVM := virt.NewColimaVirt(mocks.Injector) - pipeline.injector.Register("virtualMachine", existingVM) - - // When getting virtual machine - vm := pipeline.withVirtualMachine() - - // Then the existing VM should be returned - if vm != existingVM { - t.Error("Expected existing virtual machine to be reused") - } - }) - - t.Run("CreatesNewVMWhenRegisteredValueIsNotVirtualMachine", func(t *testing.T) { - // Given a pipeline with wrong type registered - pipeline, _ := setup(t) - mockConfigHandler := pipeline.configHandler.(*config.MockConfigHandler) - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - if key == "vm.driver" { - return "colima" - } - return "" - } - - pipeline.injector.Register("virtualMachine", "not-a-vm") - - // When getting virtual machine - vm := pipeline.withVirtualMachine() - - // Then a new VM should be created - if vm == nil { - t.Error("Expected virtual machine to be created") - } - }) -} - -func TestBasePipeline_withContainerRuntime(t *testing.T) { - setup := func(t *testing.T) (*BasePipeline, *Mocks) { - pipeline := NewBasePipeline() - - // Create mock config handler - mockConfigHandler := config.NewMockConfigHandler() - opts := &SetupOptions{ - ConfigHandler: mockConfigHandler, - } - mocks := setupMocks(t, opts) - - pipeline.injector = mocks.Injector - pipeline.configHandler = mockConfigHandler - return pipeline, mocks - } - - t.Run("ReturnsNilWhenDockerDisabled", func(t *testing.T) { - // Given a pipeline with docker disabled - pipeline, _ := setup(t) - mockConfigHandler := pipeline.configHandler.(*config.MockConfigHandler) - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - if key == "docker.enabled" { - return false - } - return false - } - - // When getting container runtime - runtime := pipeline.withContainerRuntime() - - // Then nil should be returned - if runtime != nil { - t.Error("Expected nil container runtime when docker disabled") - } - }) - - t.Run("CreatesDockerRuntimeWhenDockerEnabled", func(t *testing.T) { - // Given a pipeline with docker enabled - pipeline, _ := setup(t) - mockConfigHandler := pipeline.configHandler.(*config.MockConfigHandler) - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - if key == "docker.enabled" { - return true - } - return false - } - - // When getting container runtime - runtime := pipeline.withContainerRuntime() - - // Then docker runtime should be created and registered - if runtime == nil { - t.Error("Expected docker container runtime to be created") - } - - registered := pipeline.injector.Resolve("containerRuntime") - if registered == nil { - t.Error("Expected container runtime to be registered") - } - }) - - t.Run("ReusesExistingContainerRuntimeWhenRegistered", func(t *testing.T) { - // Given a pipeline with existing container runtime - pipeline, mocks := setup(t) - existingRuntime := virt.NewDockerVirt(mocks.Injector) - pipeline.injector.Register("containerRuntime", existingRuntime) - - // When getting container runtime - runtime := pipeline.withContainerRuntime() - - // Then the existing runtime should be returned - if runtime != existingRuntime { - t.Error("Expected existing container runtime to be reused") - } - }) - - t.Run("CreatesNewRuntimeWhenRegisteredValueIsNotContainerRuntime", func(t *testing.T) { - // Given a pipeline with wrong type registered and docker enabled - pipeline, _ := setup(t) - mockConfigHandler := pipeline.configHandler.(*config.MockConfigHandler) - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - if key == "docker.enabled" { - return true - } - return false - } - - pipeline.injector.Register("containerRuntime", "not-a-runtime") - - // When getting container runtime - runtime := pipeline.withContainerRuntime() - - // Then a new runtime should be created - if runtime == nil { - t.Error("Expected container runtime to be created") - } - }) -} - -func TestBasePipeline_withKubernetesClient(t *testing.T) { - setup := func(t *testing.T) (*BasePipeline, *Mocks) { - pipeline := NewBasePipeline() - mocks := setupMocks(t) - pipeline.injector = mocks.Injector - return pipeline, mocks - } - - t.Run("CreatesNewKubernetesClientWhenNotRegistered", func(t *testing.T) { - // Given a pipeline without kubernetes client - pipeline, _ := setup(t) - - // When getting kubernetes client - client := pipeline.withKubernetesClient() - - // Then a new client should be created and registered - if client == nil { - t.Error("Expected kubernetes client to be created") - } - - registered := pipeline.injector.Resolve("kubernetesClient") - if registered == nil { - t.Error("Expected kubernetes client to be registered") - } - }) - - t.Run("ReusesExistingKubernetesClientWhenRegistered", func(t *testing.T) { - // Given a pipeline with existing kubernetes client - pipeline, _ := setup(t) - existingClient := k8sclient.NewDynamicKubernetesClient() - pipeline.injector.Register("kubernetesClient", existingClient) - - // When getting kubernetes client - client := pipeline.withKubernetesClient() - - // Then the existing client should be returned - if client != existingClient { - t.Error("Expected existing kubernetes client to be reused") - } - }) - - t.Run("CreatesNewClientWhenRegisteredValueIsNotKubernetesClient", func(t *testing.T) { - // Given a pipeline with wrong type registered - pipeline, _ := setup(t) - pipeline.injector.Register("kubernetesClient", "not-a-client") - - // When getting kubernetes client - client := pipeline.withKubernetesClient() - - // Then a new client should be created - if client == nil { - t.Error("Expected kubernetes client to be created") - } - }) -} - -func TestBasePipeline_withKubernetesManager(t *testing.T) { - setup := func(t *testing.T) (*BasePipeline, *Mocks) { - pipeline := NewBasePipeline() - mocks := setupMocks(t) - pipeline.injector = mocks.Injector - return pipeline, mocks - } - - t.Run("CreatesNewKubernetesManagerWhenNotRegistered", func(t *testing.T) { - // Given a pipeline without kubernetes manager - pipeline, _ := setup(t) - - // When getting kubernetes manager - manager := pipeline.withKubernetesManager() - - // Then a new manager should be created and registered - if manager == nil { - t.Error("Expected kubernetes manager to be created") - } - - registered := pipeline.injector.Resolve("kubernetesManager") - if registered == nil { - t.Error("Expected kubernetes manager to be registered") - } - }) - - t.Run("ReusesExistingKubernetesManagerWhenRegistered", func(t *testing.T) { - // Given a pipeline with existing kubernetes manager - pipeline, mocks := setup(t) - existingManager := kubernetes.NewKubernetesManager(mocks.Injector) - pipeline.injector.Register("kubernetesManager", existingManager) - - // When getting kubernetes manager - manager := pipeline.withKubernetesManager() - - // Then the existing manager should be returned - if manager != existingManager { - t.Error("Expected existing kubernetes manager to be reused") - } - }) - - t.Run("CreatesNewManagerWhenRegisteredValueIsNotKubernetesManager", func(t *testing.T) { - // Given a pipeline with wrong type registered - pipeline, _ := setup(t) - pipeline.injector.Register("kubernetesManager", "not-a-manager") - - // When getting kubernetes manager - manager := pipeline.withKubernetesManager() - - // Then a new manager should be created - if manager == nil { - t.Error("Expected kubernetes manager to be created") - } - }) -} - -func TestBasePipeline_withServices(t *testing.T) { - t.Run("ReturnsEmptyWhenDockerDisabled", func(t *testing.T) { - // Given a base pipeline with Docker disabled - pipeline := NewBasePipeline() - - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - if key == "docker.enabled" { - return false - } - return false - } - pipeline.configHandler = mockConfigHandler - - // When creating services - services, err := pipeline.withServices() - - // Then no error should be returned and no services should be created - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(services) != 0 { - t.Errorf("Expected 0 services, got %d", len(services)) - } - }) - - t.Run("CreatesMultipleServicesWhenDockerEnabled", func(t *testing.T) { - // Given a base pipeline with Docker and multiple services enabled - pipeline := NewBasePipeline() - - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - if key == "cluster.driver" { - return "talos" - } - return "" - } - mockConfigHandler.GetIntFunc = func(key string, defaultValue ...int) int { - switch key { - case "cluster.controlplanes.count": - return 2 - case "cluster.workers.count": - return 3 - default: - return 1 - } - } - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - switch key { - case "docker.enabled": - return true - case "dns.enabled": - return true - case "git.livereload.enabled": - return true - case "aws.localstack.enabled": - return true - default: - return false - } - } - pipeline.configHandler = mockConfigHandler - pipeline.injector = di.NewInjector() - - // When creating services - services, err := pipeline.withServices() - - // Then no error should be returned and multiple services should be created - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - // Should have DNS, Git, AWS, 2 control plane, and 3 worker services - if len(services) != 8 { - t.Errorf("Expected 8 services, got %d", len(services)) - } - }) - - t.Run("CreatesRegistryServicesWhenDockerRegistriesConfigured", func(t *testing.T) { - // Given a base pipeline with Docker registries configured - pipeline := NewBasePipeline() - - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - if key == "docker.enabled" { - return true - } - return false - } - mockConfigHandler.GetConfigFunc = func() *v1alpha1.Context { - return &v1alpha1.Context{ - Docker: &docker.DockerConfig{ - Registries: map[string]docker.RegistryConfig{ - "registry1": {}, - "registry2": {}, - }, - }, - } - } - pipeline.configHandler = mockConfigHandler - pipeline.injector = di.NewInjector() - - // When creating services - services, err := pipeline.withServices() - - // Then no error should be returned and registry services should be created - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(services) != 2 { - t.Errorf("Expected 2 services, got %d", len(services)) - } - }) - - t.Run("CreatesOmniClusterServices", func(t *testing.T) { - // Given a base pipeline with Omni cluster provider - pipeline := NewBasePipeline() - - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - if key == "docker.enabled" { - return true - } - return false - } - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - if key == "cluster.driver" { - return "omni" - } - return "" - } - mockConfigHandler.GetIntFunc = func(key string, defaultValue ...int) int { - switch key { - case "cluster.controlplanes.count": - return 1 - case "cluster.workers.count": - return 2 - default: - return 1 - } - } - pipeline.configHandler = mockConfigHandler - pipeline.injector = di.NewInjector() - - // When creating services - services, err := pipeline.withServices() - - // Then no error should be returned and cluster services should be created - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - // Should have 1 control plane and 2 worker services - if len(services) != 3 { - t.Errorf("Expected 3 services, got %d", len(services)) - } - }) - - t.Run("ReturnsErrorWhenConfigHandlerIsNil", func(t *testing.T) { - // Given a base pipeline with nil config handler - pipeline := NewBasePipeline() - pipeline.configHandler = nil - - // When creating services - services, err := pipeline.withServices() - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "config handler not initialized" { - t.Errorf("Expected 'config handler not initialized', got: %v", err) - } - if services != nil { - t.Error("Expected nil services") - } - }) -} - -func TestBasePipeline_withTerraformResolvers(t *testing.T) { - setup := func(t *testing.T) (*BasePipeline, *Mocks) { - pipeline := NewBasePipeline() - mocks := setupMocks(t) - return pipeline, mocks - } - - t.Run("ReturnsEmptyWhenTerraformDisabled", func(t *testing.T) { - // Given a pipeline with terraform disabled - pipeline, mocks := setup(t) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Initialize failed: %v", err) - } - - // When getting terraform resolvers - resolvers, err := pipeline.withTerraformResolvers() - - // Then no error should occur and resolvers should be empty - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(resolvers) != 0 { - t.Errorf("Expected 0 resolvers, got %d", len(resolvers)) - } - }) - - t.Run("CreatesTerraformResolversWhenTerraformEnabled", func(t *testing.T) { - // Given a pipeline with terraform enabled - pipeline, mocks := setup(t) - mocks.ConfigHandler.LoadConfigString(` -apiVersion: v1alpha1 -contexts: - mock-context: - terraform: - enabled: true`) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Initialize failed: %v", err) - } - - // When getting terraform resolvers - resolvers, err := pipeline.withTerraformResolvers() - - // Then no error should occur and both resolvers should be created - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(resolvers) != 2 { - t.Errorf("Expected 2 resolvers, got %d", len(resolvers)) - } - - // And resolvers should be registered in the DI container - if mocks.Injector.Resolve("standardModuleResolver") == nil { - t.Error("Expected standardModuleResolver to be registered") - } - if mocks.Injector.Resolve("ociModuleResolver") == nil { - t.Error("Expected ociModuleResolver to be registered") - } - }) - - t.Run("ReturnsErrorWhenConfigHandlerIsNil", func(t *testing.T) { - // Given a pipeline with nil config handler - pipeline := NewBasePipeline() - pipeline.configHandler = nil - - // When getting terraform resolvers - _, err := pipeline.withTerraformResolvers() - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "config handler not initialized") { - t.Errorf("Expected config handler error, got %v", err) - } - }) -} - -func TestBasePipeline_withGenerators(t *testing.T) { - setup := func(t *testing.T) (*BasePipeline, *Mocks) { - pipeline := NewBasePipeline() - mocks := setupMocks(t) - return pipeline, mocks - } - - t.Run("CreatesGenerators", func(t *testing.T) { - // Given a pipeline - pipeline, mocks := setup(t) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Initialize failed: %v", err) - } - - // When getting generators - generators, err := pipeline.withGenerators() - - // Then no error should occur and generators should be created - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(generators) == 0 { - t.Error("Expected generators to be created") - } - - // And git generator should be registered - registered := mocks.Injector.Resolve("gitGenerator") - if registered == nil { - t.Error("Expected git generator to be registered") - } - }) - - t.Run("CreatesTerraformGeneratorWhenTerraformEnabled", func(t *testing.T) { - // Given a pipeline with terraform enabled - pipeline, mocks := setup(t) - mocks.ConfigHandler.LoadConfigString(` -apiVersion: v1alpha1 -contexts: - mock-context: - terraform: - enabled: true`) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Initialize failed: %v", err) - } - - // When getting generators - generators, err := pipeline.withGenerators() - - // Then no error should occur and terraform generator should be included - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(generators) < 2 { - t.Error("Expected at least 2 generators (git + terraform)") - } - - // And terraform generator should be registered - registered := mocks.Injector.Resolve("terraformGenerator") - if registered == nil { - t.Error("Expected terraform generator to be registered") - } - }) -} - -func TestBasePipeline_withToolsManager(t *testing.T) { - setup := func(t *testing.T) (*BasePipeline, *Mocks) { - pipeline := NewBasePipeline() - mocks := setupMocks(t) - return pipeline, mocks - } - - t.Run("CreatesNewToolsManagerWhenNotRegistered", func(t *testing.T) { - // Given a pipeline without tools manager registered - pipeline, mocks := setup(t) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Initialize failed: %v", err) - } - - // When getting tools manager - toolsManager := pipeline.withToolsManager() - - // Then a new tools manager should be created - if toolsManager == nil { - t.Error("Expected tools manager to not be nil") - } - - // And it should be registered in the injector - registered := mocks.Injector.Resolve("toolsManager") - if registered == nil { - t.Error("Expected tools manager to be registered") - } - }) - - t.Run("ReusesExistingToolsManagerWhenRegistered", func(t *testing.T) { - // Given a pipeline with tools manager already registered - pipeline, mocks := setup(t) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Initialize failed: %v", err) - } - - // And an existing tools manager - existingManager := pipeline.withToolsManager() - - // When getting tools manager again - toolsManager := pipeline.withToolsManager() - - // Then the same tools manager should be returned - if toolsManager != existingManager { - t.Error("Expected to reuse existing tools manager") - } - }) -} - -func TestBasePipeline_withClusterClient(t *testing.T) { - setup := func(t *testing.T) (*BasePipeline, *Mocks) { - pipeline := NewBasePipeline() - mocks := setupMocks(t) - return pipeline, mocks - } - - t.Run("CreatesNewClusterClientWhenNotRegistered", func(t *testing.T) { - // Given a pipeline without cluster client registered - pipeline, mocks := setup(t) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Initialize failed: %v", err) - } - - // When getting cluster client - clusterClient := pipeline.withClusterClient() - - // Then a new cluster client should be created - if clusterClient == nil { - t.Error("Expected cluster client to not be nil") - } - - // And it should be registered in the injector - registered := mocks.Injector.Resolve("clusterClient") - if registered == nil { - t.Error("Expected cluster client to be registered") - } - }) - - t.Run("ReusesExistingClusterClientWhenRegistered", func(t *testing.T) { - // Given a pipeline with cluster client already registered - pipeline, mocks := setup(t) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Initialize failed: %v", err) - } - - // And an existing cluster client - existingClient := pipeline.withClusterClient() - - // When getting cluster client again - clusterClient := pipeline.withClusterClient() - - // Then the same cluster client should be returned - if clusterClient != existingClient { - t.Error("Expected to reuse existing cluster client") - } - }) -} - -// ============================================================================= -// Template Processing Tests -// ============================================================================= - -func TestBasePipeline_prepareTemplateData(t *testing.T) { - t.Run("Priority1_ExplicitBlueprintOverridesLocalTemplates", func(t *testing.T) { - // Given a pipeline with both explicit blueprint and local templates - pipeline := NewBasePipeline() - pipeline.injector = di.NewInjector() - - // Mock artifact builder that succeeds - mockArtifactBuilder := artifact.NewMockArtifact() - expectedOCIData := map[string][]byte{ - "blueprint.jsonnet": []byte("{ explicit: 'oci-data' }"), - } - mockArtifactBuilder.GetTemplateDataFunc = func(ociRef string) (map[string][]byte, error) { - return expectedOCIData, nil - } - pipeline.artifactBuilder = mockArtifactBuilder - - // Mock blueprint handler with local templates - mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) - mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { - return map[string][]byte{ - "blueprint.jsonnet": []byte("{ local: 'template-data' }"), - }, nil - } - pipeline.injector.Register("blueprintHandler", mockBlueprintHandler) - - // Create context with explicit blueprint value - ctx := context.WithValue(context.Background(), "blueprint", "oci://registry.example.com/blueprint:latest") - - // When prepareTemplateData is called - templateData, err := pipeline.prepareTemplateData(ctx) - - // Then should use explicit blueprint, not local templates - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(templateData) != 1 { - t.Errorf("Expected 1 template file, got %d", len(templateData)) - } - if string(templateData["blueprint.jsonnet"]) != "{ explicit: 'oci-data' }" { - t.Error("Expected explicit blueprint data to override local templates") - } - }) - - t.Run("Priority1_ExplicitBlueprintFailsWithError", func(t *testing.T) { - // Given a pipeline with explicit blueprint that fails - pipeline := NewBasePipeline() - pipeline.injector = di.NewInjector() - - mockArtifactBuilder := artifact.NewMockArtifact() - mockArtifactBuilder.GetTemplateDataFunc = func(ociRef string) (map[string][]byte, error) { - return nil, fmt.Errorf("OCI pull failed") - } - pipeline.artifactBuilder = mockArtifactBuilder - - ctx := context.WithValue(context.Background(), "blueprint", "oci://registry.example.com/blueprint:latest") - - // When prepareTemplateData is called - templateData, err := pipeline.prepareTemplateData(ctx) - - // Then should return error - if err == nil { - t.Fatal("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to get template data from blueprint") { - t.Errorf("Expected error to contain 'failed to get template data from blueprint', got %v", err) - } - if templateData != nil { - t.Error("Expected nil template data on error") - } - }) - - t.Run("Priority2_LocalTemplatesWhenNoExplicitBlueprint", func(t *testing.T) { - // Given a pipeline with local templates but no explicit blueprint - pipeline := NewBasePipeline() - injector := di.NewInjector() - - mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) - expectedLocalData := map[string][]byte{ - "blueprint.jsonnet": []byte("{ local: 'template-data' }"), - } - mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { - return expectedLocalData, nil - } - injector.Register("blueprintHandler", mockBlueprintHandler) - - // Initialize the pipeline to set up all components - if err := pipeline.Initialize(injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When prepareTemplateData is called with no blueprint context - templateData, err := pipeline.prepareTemplateData(context.Background()) - - // Then should use local template data - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(templateData) != 1 { - t.Errorf("Expected 1 template file, got %d", len(templateData)) - } - if string(templateData["blueprint.jsonnet"]) != "{ local: 'template-data' }" { - t.Error("Expected local template data") - } - }) - - t.Run("Priority3_DefaultOCIURLWhenNoLocalTemplates", func(t *testing.T) { - // Given a pipeline with no local templates and artifact builder - pipeline := NewBasePipeline() - pipeline.injector = di.NewInjector() - - // Mock artifact builder for default OCI URL - mockArtifactBuilder := artifact.NewMockArtifact() - expectedDefaultOCIData := map[string][]byte{ - "blueprint.jsonnet": []byte("{ default: 'oci-data' }"), - } - var receivedOCIRef string - mockArtifactBuilder.GetTemplateDataFunc = func(ociRef string) (map[string][]byte, error) { - receivedOCIRef = ociRef - return expectedDefaultOCIData, nil - } - pipeline.artifactBuilder = mockArtifactBuilder - - // Mock blueprint handler with no local templates - mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) - mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { - return make(map[string][]byte), nil // Empty local templates - } - pipeline.injector.Register("blueprintHandler", mockBlueprintHandler) - - // When prepareTemplateData is called with no blueprint context - templateData, err := pipeline.prepareTemplateData(context.Background()) - - // Then should use default OCI URL - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(templateData) != 1 { - t.Errorf("Expected 1 template file, got %d", len(templateData)) - } - if string(templateData["blueprint.jsonnet"]) != "{ default: 'oci-data' }" { - t.Error("Expected default OCI blueprint data") - } - // Verify the correct default OCI URL was used - if !strings.Contains(receivedOCIRef, "ghcr.io/windsorcli/core") { - t.Errorf("Expected default OCI URL to be used, got %s", receivedOCIRef) - } - }) - - t.Run("Priority3_LocalTemplateDirectoryExistsUsesLocalEvenIfEmpty", func(t *testing.T) { - // Given a pipeline with contexts/_template directory that exists but has no .jsonnet files - pipeline := NewBasePipeline() - injector := di.NewInjector() - - // Mock shell to return project root - mockShell := shell.NewMockShell(nil) - mockShell.GetProjectRootFunc = func() (string, error) { - return "/test/project", nil - } - injector.Register("shell", mockShell) - - // Mock shims to simulate contexts/_template directory exists - shims := &Shims{ - Stat: func(path string) (os.FileInfo, error) { - if path == "/test/project/contexts/_template" { - return &mockInitFileInfo{name: "_template", isDir: true}, nil - } - return nil, os.ErrNotExist - }, - } - injector.Register("shims", shims) - - // Mock blueprint handler with empty local templates (no .jsonnet files) - mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) - mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { - // Return empty map but with values.yaml data merged in - return map[string][]byte{ - "values": []byte("external_domain: local.test"), - }, nil - } - injector.Register("blueprintHandler", mockBlueprintHandler) - - // Mock artifact builder (should NOT be called) - mockArtifactBuilder := artifact.NewMockArtifact() - mockArtifactBuilder.GetTemplateDataFunc = func(ociRef string) (map[string][]byte, error) { - t.Error("Artifact builder should not be called when local template directory exists") - return nil, fmt.Errorf("should not be called") - } - injector.Register("artifactBuilder", mockArtifactBuilder) - - // Initialize the pipeline to set up all components - if err := pipeline.Initialize(injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When prepareTemplateData is called with no blueprint context - templateData, err := pipeline.prepareTemplateData(context.Background()) - - // Then should use local template data even if it only contains values.yaml - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(templateData) != 1 { - t.Errorf("Expected 1 template file (values), got %d", len(templateData)) - } - if string(templateData["values"]) != "external_domain: local.test" { - t.Error("Expected local values data") - } - }) - - t.Run("Priority4_EmbeddedDefaultWhenNoArtifactBuilder", func(t *testing.T) { - // Given a pipeline with no artifact builder - pipeline := NewBasePipeline() - injector := di.NewInjector() - - // Mock config handler (needed for determineContextName) - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetContextFunc = func() string { - return "local" - } - injector.Register("configHandler", mockConfigHandler) - - // Mock blueprint handler with no local templates but default template - mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) - mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { - return make(map[string][]byte), nil // Empty local templates - } - expectedDefaultData := map[string][]byte{ - "blueprint.jsonnet": []byte("{ embedded: 'default-template' }"), - } - mockBlueprintHandler.GetDefaultTemplateDataFunc = func(contextName string) (map[string][]byte, error) { - return expectedDefaultData, nil - } - injector.Register("blueprintHandler", mockBlueprintHandler) - - // Initialize the pipeline to set up all components - if err := pipeline.Initialize(injector, context.Background()); err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // Set artifact builder to nil to test the "no artifact builder" scenario - pipeline.artifactBuilder = nil - - // When prepareTemplateData is called - templateData, err := pipeline.prepareTemplateData(context.Background()) - - // Then should use embedded default template - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(templateData) != 1 { - t.Errorf("Expected 1 template file, got %d", len(templateData)) - } - if string(templateData["blueprint.jsonnet"]) != "{ embedded: 'default-template' }" { - t.Error("Expected embedded default template data") - } - }) - - t.Run("ReturnsEmptyMapWhenNothingAvailable", func(t *testing.T) { - // Given a pipeline with no blueprint handler and no artifact builder - pipeline := NewBasePipeline() - pipeline.injector = di.NewInjector() - - // Set up config handler - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetContextFunc = func() string { - return "local" - } - pipeline.configHandler = mockConfigHandler - - // Register a mock blueprint handler that returns empty data - mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) - mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { - return make(map[string][]byte), nil - } - pipeline.injector.Register("blueprintHandler", mockBlueprintHandler) - - // When prepareTemplateData is called - templateData, err := pipeline.prepareTemplateData(context.Background()) - - // Then should return empty map - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if templateData == nil { - t.Error("Expected non-nil template data") - } - if len(templateData) != 0 { - t.Error("Expected empty template data") - } - }) - - t.Run("HandlesInvalidOCIReference", func(t *testing.T) { - // Given a pipeline with invalid OCI reference - pipeline := NewBasePipeline() - pipeline.injector = di.NewInjector() - - mockArtifactBuilder := artifact.NewMockArtifact() - pipeline.artifactBuilder = mockArtifactBuilder - - // Create context with invalid blueprint value - ctx := context.WithValue(context.Background(), "blueprint", "invalid-oci-reference") - - // When prepareTemplateData is called - templateData, err := pipeline.prepareTemplateData(ctx) - - // Then should return error for invalid reference - if err == nil { - t.Fatal("Expected error for invalid OCI reference, got nil") - } - if !strings.Contains(err.Error(), "failed to parse blueprint reference") { - t.Errorf("Expected error to contain 'failed to parse blueprint reference', got %v", err) - } - if templateData != nil { - t.Error("Expected nil template data on error") - } - }) -} - -func TestBasePipeline_determineContextName(t *testing.T) { - t.Run("ReturnsContextNameFromContext", func(t *testing.T) { - // Given a pipeline - pipeline := NewBasePipeline() - - // And context with contextName - ctx := context.WithValue(context.Background(), "contextName", "test-context") - - // When determineContextName is called - result := pipeline.determineContextName(ctx) - - // Then should return context name from context - if result != "test-context" { - t.Errorf("Expected 'test-context', got %s", result) - } - }) - - t.Run("ReturnsContextFromConfigHandler", func(t *testing.T) { - // Given a pipeline with config handler - pipeline := NewBasePipeline() - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetContextFunc = func() string { - return "config-context" - } - pipeline.configHandler = mockConfigHandler - - // When determineContextName is called - result := pipeline.determineContextName(context.Background()) - - // Then should return context from config handler - if result != "config-context" { - t.Errorf("Expected 'config-context', got %s", result) - } - }) - - t.Run("ReturnsLocalWhenNoContextSet", func(t *testing.T) { - // Given a pipeline with no context set - pipeline := NewBasePipeline() - - // When determineContextName is called - result := pipeline.determineContextName(context.Background()) - - // Then should return "local" - if result != "local" { - t.Errorf("Expected 'local', got %s", result) - } - }) - - t.Run("ReturnsLocalWhenContextIsLocal", func(t *testing.T) { - // Given a pipeline with config handler returning "local" - pipeline := NewBasePipeline() - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetContextFunc = func() string { - return "local" - } - pipeline.configHandler = mockConfigHandler - - // When determineContextName is called - result := pipeline.determineContextName(context.Background()) - - // Then should return "local" - if result != "local" { - t.Errorf("Expected 'local', got %s", result) - } - }) -} diff --git a/pkg/pipelines/shims.go b/pkg/pipelines/shims.go deleted file mode 100644 index acffef3e3..000000000 --- a/pkg/pipelines/shims.go +++ /dev/null @@ -1,34 +0,0 @@ -package pipelines - -import "os" - -// Shims provides a testable interface for system operations used by pipelines. -// This struct-based approach allows for better isolation during testing by enabling -// dependency injection of mock implementations for file system and environment operations. -// Each pipeline can use its own Shims instance with customized behavior for testing scenarios. -type Shims struct { - Stat func(name string) (os.FileInfo, error) - Getenv func(key string) string - Setenv func(key, value string) error - ReadDir func(name string) ([]os.DirEntry, error) - ReadFile func(name string) ([]byte, error) - RemoveAll func(path string) error - MkdirAll func(path string, perm os.FileMode) error - WriteFile func(name string, data []byte, perm os.FileMode) error -} - -// NewShims creates a new Shims instance with default system call implementations. -// The returned instance provides direct access to os package functions and can be -// used in production environments or as a base for creating test-specific variants. -func NewShims() *Shims { - return &Shims{ - Stat: os.Stat, - Getenv: os.Getenv, - Setenv: os.Setenv, - ReadDir: os.ReadDir, - ReadFile: os.ReadFile, - RemoveAll: os.RemoveAll, - MkdirAll: os.MkdirAll, - WriteFile: os.WriteFile, - } -} diff --git a/pkg/pipelines/up.go b/pkg/pipelines/up.go deleted file mode 100644 index a1ee54692..000000000 --- a/pkg/pipelines/up.go +++ /dev/null @@ -1,238 +0,0 @@ -package pipelines - -import ( - "context" - "fmt" - "os" - - 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" - terraforminfra "github.com/windsorcli/cli/pkg/provisioner/terraform" - "github.com/windsorcli/cli/pkg/workstation/network" - "github.com/windsorcli/cli/pkg/workstation/services" - "github.com/windsorcli/cli/pkg/workstation/virt" -) - -// The UpPipeline is a specialized component that manages the infrastructure deployment phase -// of the Windsor environment setup. It focuses on tools installation, virtual machine startup, -// container runtime startup, network configuration, and stack deployment. -// The UpPipeline assumes that env and init pipelines have already been executed and handled -// environment variables, secrets, and basic configuration setup. - -// ============================================================================= -// Types -// ============================================================================= - -// UpPipeline provides infrastructure deployment functionality for the up command -type UpPipeline struct { - BasePipeline - toolsManager tools.ToolsManager - virtualMachine virt.VirtualMachine - containerRuntime virt.ContainerRuntime - networkManager network.NetworkManager - stack terraforminfra.Stack - envPrinters []envvars.EnvPrinter -} - -// ============================================================================= -// Constructor -// ============================================================================= - -// NewUpPipeline creates a new UpPipeline instance -func NewUpPipeline() *UpPipeline { - return &UpPipeline{ - BasePipeline: *NewBasePipeline(), - } -} - -// ============================================================================= -// Public Methods -// ============================================================================= - -// Initialize sets up the up pipeline components including tools manager, virtual machine, -// container runtime, network manager, stack, and blueprint handler. It only initializes -// the components needed for the infrastructure deployment phase, since env and init -// pipelines handle the earlier setup phases. -// Initialize sets up the up pipeline components by first constructing all dependencies -// using the "with" methods, then initializing them in sequence. This ensures all -// dependencies are present before any initialization logic is invoked. -func (p *UpPipeline) Initialize(injector di.Injector, ctx context.Context) error { - if err := p.BasePipeline.Initialize(injector, ctx); err != nil { - return err - } - - p.toolsManager = p.withToolsManager() - p.virtualMachine = p.withVirtualMachine() - p.containerRuntime = p.withContainerRuntime() - p.networkManager = p.withNetworking() - p.stack = p.withStack() - - envPrinters, err := p.withEnvPrinters() - if err != nil { - return fmt.Errorf("failed to create env printers: %w", err) - } - p.envPrinters = envPrinters - - for _, envPrinter := range p.envPrinters { - if err := envPrinter.Initialize(); err != nil { - return fmt.Errorf("failed to initialize env printer: %w", err) - } - } - - if p.toolsManager != nil { - if err := p.toolsManager.Initialize(); err != nil { - return fmt.Errorf("failed to initialize tools manager: %w", err) - } - } - if p.virtualMachine != nil { - if err := p.virtualMachine.Initialize(); err != nil { - return fmt.Errorf("failed to initialize virtual machine: %w", err) - } - } - if p.containerRuntime != nil { - if err := p.containerRuntime.Initialize(); err != nil { - return fmt.Errorf("failed to initialize container runtime: %w", err) - } - } - - if secureShell := p.injector.Resolve("secureShell"); secureShell != nil { - if secureShellInterface, ok := secureShell.(shell.Shell); ok { - if err := secureShellInterface.Initialize(); err != nil { - return fmt.Errorf("failed to initialize secure shell: %w", err) - } - } - } - - if p.networkManager != nil { - resolvedServices, _ := p.injector.ResolveAll(new(services.Service)) - serviceList := make([]services.Service, 0, len(resolvedServices)) - for _, svc := range resolvedServices { - if s, ok := svc.(services.Service); ok { - serviceList = append(serviceList, s) - } - } - if err := p.networkManager.Initialize(serviceList); err != nil { - return fmt.Errorf("failed to initialize network manager: %w", err) - } - } - if p.stack != nil { - if err := p.stack.Initialize(); err != nil { - return fmt.Errorf("failed to initialize stack: %w", err) - } - } - - return nil -} - -// Execute performs the infrastructure deployment operations including tools installation, -// VM/container startup, networking configuration, stack deployment, and optional blueprint installation. -func (p *UpPipeline) Execute(ctx context.Context) error { - // Set NO_CACHE environment variable to prevent caching during up operations - if err := p.shims.Setenv("NO_CACHE", "true"); err != nil { - return fmt.Errorf("Error setting NO_CACHE environment variable: %w", err) - } - - // Set environment variables globally in the process (similar to old controller.SetEnvironmentVariables()) - for _, envPrinter := range p.envPrinters { - envVars, err := envPrinter.GetEnvVars() - if err != nil { - return fmt.Errorf("error getting environment variables: %w", err) - } - for key, value := range envVars { - if err := p.shims.Setenv(key, value); err != nil { - return fmt.Errorf("error setting environment variable %s: %w", key, err) - } - } - } - - // Check and install tools - if p.toolsManager != nil { - if err := p.toolsManager.Check(); err != nil { - return fmt.Errorf("Error checking tools: %w", err) - } - if err := p.toolsManager.Install(); err != nil { - return fmt.Errorf("Error installing tools: %w", err) - } - } - - // Start virtual machine if using colima - vmDriverConfig := p.configHandler.GetString("vm.driver") - if vmDriverConfig == "colima" { - if p.virtualMachine == nil { - return fmt.Errorf("No virtual machine found") - } - if err := p.virtualMachine.Up(); err != nil { - return fmt.Errorf("Error running virtual machine Up command: %w", err) - } - } - - // Start container runtime if enabled - containerRuntimeEnabled := p.configHandler.GetBool("docker.enabled") - if containerRuntimeEnabled { - if p.containerRuntime == nil { - return fmt.Errorf("No container runtime found") - } - if err := p.containerRuntime.Up(); err != nil { - return fmt.Errorf("Error running container runtime Up command: %w", err) - } - } - - // Configure networking - if p.networkManager == nil { - return fmt.Errorf("No network manager found") - } - - // Configure networking for the virtual machine - if vmDriverConfig == "colima" { - if err := p.networkManager.ConfigureGuest(); err != nil { - return fmt.Errorf("Error configuring guest network: %w", err) - } - if err := p.networkManager.ConfigureHostRoute(); err != nil { - return fmt.Errorf("Error configuring host network: %w", err) - } - } - - // Configure DNS settings - if dnsEnabled := p.configHandler.GetBool("dns.enabled"); dnsEnabled { - fmt.Fprintf(os.Stderr, "→ ⚠️ DNS configuration may require administrative privileges\n") - - if err := p.networkManager.ConfigureDNS(); err != nil { - return fmt.Errorf("Error configuring DNS: %w", err) - } - } - - // Bring up the stack - if p.stack == nil { - return fmt.Errorf("No stack found") - } - blueprintHandler := p.withBlueprintHandler() - if blueprintHandler == nil { - return fmt.Errorf("No blueprint handler found") - } - if err := blueprintHandler.Initialize(); err != nil { - return fmt.Errorf("failed to initialize blueprint handler: %w", err) - } - if err := blueprintHandler.LoadConfig(); err != nil { - return fmt.Errorf("failed to load blueprint config: %w", err) - } - if err := blueprintHandler.LoadBlueprint(); err != nil { - return fmt.Errorf("failed to load blueprint: %w", err) - } - blueprint := blueprintHandler.Generate() - if err := p.stack.Up(blueprint); err != nil { - return fmt.Errorf("Error running stack Up command: %w", err) - } - - // Print success message - fmt.Fprintln(os.Stderr, "Windsor environment set up successfully.") - - return nil -} - -// ============================================================================= -// Interface Compliance -// ============================================================================= - -var _ Pipeline = (*UpPipeline)(nil) diff --git a/pkg/pipelines/up_test.go b/pkg/pipelines/up_test.go deleted file mode 100644 index 92cd73d31..000000000 --- a/pkg/pipelines/up_test.go +++ /dev/null @@ -1,769 +0,0 @@ -package pipelines - -import ( - "context" - "fmt" - "testing" - - blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" - "github.com/windsorcli/cli/pkg/context/config" - envvars "github.com/windsorcli/cli/pkg/context/env" - "github.com/windsorcli/cli/pkg/context/shell" - "github.com/windsorcli/cli/pkg/context/tools" - terraforminfra "github.com/windsorcli/cli/pkg/provisioner/terraform" - "github.com/windsorcli/cli/pkg/workstation/network" - "github.com/windsorcli/cli/pkg/workstation/services" - "github.com/windsorcli/cli/pkg/workstation/virt" -) - -// ============================================================================= -// Test Setup -// ============================================================================= - -type UpMocks struct { - *Mocks - ToolsManager *tools.MockToolsManager - VirtualMachine *virt.MockVirt - ContainerRuntime *virt.MockVirt - NetworkManager *network.MockNetworkManager - Stack *terraforminfra.MockStack -} - -func setupUpMocks(t *testing.T, opts ...*SetupOptions) *UpMocks { - t.Helper() - - // Create setup options, preserving any provided options - setupOptions := &SetupOptions{} - if len(opts) > 0 && opts[0] != nil { - setupOptions = opts[0] - } - - baseMocks := setupMocks(t, setupOptions) - - // Initialize the config handler if it's a real one - if setupOptions.ConfigHandler == nil { - configHandler := baseMocks.ConfigHandler - configHandler.SetContext("mock-context") - - // Load base config with up-specific settings - configYAML := ` -apiVersion: v1alpha1 -contexts: - mock-context: - dns: - domain: mock.domain.com - enabled: true - network: - cidr_block: 10.0.0.0/24 - docker: - enabled: true - vm: - driver: colima - tools: - enabled: true` - - if err := configHandler.LoadConfigString(configYAML); err != nil { - t.Fatalf("Failed to load config: %v", err) - } - } - - // Setup tools manager mock - 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) - - // Setup virtual machine mock - mockVirtualMachine := virt.NewMockVirt() - mockVirtualMachine.InitializeFunc = func() error { return nil } - mockVirtualMachine.UpFunc = func(verbose ...bool) error { return nil } - baseMocks.Injector.Register("virtualMachine", mockVirtualMachine) - - // Setup container runtime mock - mockContainerRuntime := virt.NewMockVirt() - mockContainerRuntime.InitializeFunc = func() error { return nil } - mockContainerRuntime.UpFunc = func(verbose ...bool) error { return nil } - baseMocks.Injector.Register("containerRuntime", mockContainerRuntime) - - // Setup network manager mock - mockNetworkManager := network.NewMockNetworkManager() - mockNetworkManager.InitializeFunc = func([]services.Service) error { return nil } - mockNetworkManager.ConfigureGuestFunc = func() error { return nil } - mockNetworkManager.ConfigureHostRouteFunc = func() error { return nil } - mockNetworkManager.ConfigureDNSFunc = func() error { return nil } - baseMocks.Injector.Register("networkManager", mockNetworkManager) - - // Setup stack mock - mockStack := terraforminfra.NewMockStack(baseMocks.Injector) - mockStack.InitializeFunc = func() error { return nil } - mockStack.UpFunc = func(blueprint *blueprintv1alpha1.Blueprint) error { return nil } - baseMocks.Injector.Register("stack", mockStack) - - // Setup terraform env mock - mockTerraformEnv := envvars.NewMockEnvPrinter() - mockTerraformEnv.InitializeFunc = func() error { return nil } - mockTerraformEnv.GetEnvVarsFunc = func() (map[string]string, error) { return map[string]string{}, nil } - baseMocks.Injector.Register("terraformEnv", mockTerraformEnv) - - // Add GetSessionTokenFunc to the existing shell mock - baseMocks.Shell.GetSessionTokenFunc = func() (string, error) { return "mock-session-token", nil } - - return &UpMocks{ - Mocks: baseMocks, - ToolsManager: mockToolsManager, - VirtualMachine: mockVirtualMachine, - ContainerRuntime: mockContainerRuntime, - NetworkManager: mockNetworkManager, - Stack: mockStack, - } -} - -// ============================================================================= -// Test Constructor -// ============================================================================= - -func TestNewUpPipeline(t *testing.T) { - t.Run("CreatesWithDefaults", func(t *testing.T) { - // Given creating a new up pipeline - pipeline := NewUpPipeline() - - // Then pipeline should not be nil - if pipeline == nil { - t.Fatal("Expected pipeline to not be nil") - } - }) -} - -// ============================================================================= -// Test Public Methods - Initialize -// ============================================================================= - -func TestUpPipeline_Initialize(t *testing.T) { - setup := func(t *testing.T, opts ...*SetupOptions) (*UpPipeline, *UpMocks) { - t.Helper() - pipeline := NewUpPipeline() - mocks := setupUpMocks(t, opts...) - return pipeline, mocks - } - - t.Run("InitializesSuccessfully", func(t *testing.T) { - // Given an up pipeline with mock dependencies - pipeline, mocks := setup(t) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - // Test initialization failures - initFailureTests := []struct { - name string - setupMock func(*UpMocks) - expectedErr string - }{ - { - name: "ReturnsErrorWhenShellInitializeFails", - setupMock: func(mocks *UpMocks) { - mockShell := shell.NewMockShell() - mockShell.InitializeFunc = func() error { - return fmt.Errorf("shell initialization failed") - } - mocks.Injector.Register("shell", mockShell) - }, - expectedErr: "failed to initialize shell: shell initialization failed", - }, - { - name: "ReturnsErrorWhenToolsManagerInitializeFails", - setupMock: func(mocks *UpMocks) { - mocks.ToolsManager.InitializeFunc = func() error { - return fmt.Errorf("tools manager failed") - } - }, - expectedErr: "failed to initialize tools manager: tools manager failed", - }, - { - name: "ReturnsErrorWhenVirtualMachineInitializeFails", - setupMock: func(mocks *UpMocks) { - mocks.VirtualMachine.InitializeFunc = func() error { - return fmt.Errorf("virtual machine failed") - } - }, - expectedErr: "failed to initialize virtual machine: virtual machine failed", - }, - { - name: "ReturnsErrorWhenContainerRuntimeInitializeFails", - setupMock: func(mocks *UpMocks) { - mocks.ContainerRuntime.InitializeFunc = func() error { - return fmt.Errorf("container runtime failed") - } - }, - expectedErr: "failed to initialize container runtime: container runtime failed", - }, - { - name: "ReturnsErrorWhenNetworkManagerInitializeFails", - setupMock: func(mocks *UpMocks) { - mocks.NetworkManager.InitializeFunc = func([]services.Service) error { - return fmt.Errorf("network manager failed") - } - }, - expectedErr: "failed to initialize network manager: network manager failed", - }, - { - name: "ReturnsErrorWhenStackInitializeFails", - setupMock: func(mocks *UpMocks) { - mocks.Stack.InitializeFunc = func() error { - return fmt.Errorf("stack failed") - } - }, - expectedErr: "failed to initialize stack: stack failed", - }, - } - - for _, tt := range initFailureTests { - t.Run(tt.name, func(t *testing.T) { - // Given an up pipeline with failing component - pipeline, mocks := setup(t) - tt.setupMock(mocks) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != tt.expectedErr { - t.Errorf("Expected error %q, got %q", tt.expectedErr, err.Error()) - } - }) - } - - t.Run("InitializesSecureShellWhenRegistered", func(t *testing.T) { - // Given an up pipeline with secure shell registered - pipeline, mocks := setup(t) - - // Create mock secure shell - mockSecureShell := shell.NewMockShell() - secureShellInitialized := false - mockSecureShell.InitializeFunc = func() error { - secureShellInitialized = true - return nil - } - mocks.Injector.Register("secureShell", mockSecureShell) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And secure shell should be initialized - if !secureShellInitialized { - t.Error("Expected secure shell to be initialized") - } - }) - - t.Run("ReturnsErrorWhenSecureShellInitializeFails", func(t *testing.T) { - // Given an up pipeline with failing secure shell - pipeline, mocks := setup(t) - - // Create mock secure shell that fails to initialize - mockSecureShell := shell.NewMockShell() - mockSecureShell.InitializeFunc = func() error { - return fmt.Errorf("secure shell failed") - } - mocks.Injector.Register("secureShell", mockSecureShell) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "failed to initialize secure shell: secure shell failed" { - t.Errorf("Expected secure shell error, got %q", err.Error()) - } - }) - - t.Run("SkipsSecureShellWhenNotRegistered", func(t *testing.T) { - // Given an up pipeline without secure shell registered - pipeline, mocks := setup(t) - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("SkipsSecureShellWhenRegisteredTypeIsIncorrect", func(t *testing.T) { - // Given an up pipeline with incorrectly typed secure shell - pipeline, mocks := setup(t) - - // Register something that's not a shell.Shell - mocks.Injector.Register("secureShell", "not-a-shell") - - // When initializing the pipeline - err := pipeline.Initialize(mocks.Injector, context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) -} - -// ============================================================================= -// Test Public Methods - Execute -// ============================================================================= - -func TestUpPipeline_Execute(t *testing.T) { - setup := func(t *testing.T, opts ...*SetupOptions) (*UpPipeline, *UpMocks) { - t.Helper() - pipeline := NewUpPipeline() - mocks := setupUpMocks(t, opts...) - - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - return pipeline, mocks - } - - t.Run("ExecutesSuccessfully", func(t *testing.T) { - // Given a properly initialized UpPipeline - pipeline, mocks := setup(t) - - // Setup shims to allow NO_CACHE environment variable setting - mocks.Shims.Setenv = func(key, value string) error { - return nil - } - - // When Execute is called - err := pipeline.Execute(context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("ExecutesWithVerboseFlag", func(t *testing.T) { - // Given a pipeline with verbose flag set during initialization - pipeline := NewUpPipeline() - mocks := setupUpMocks(t) - - // Setup shims to allow NO_CACHE environment variable setting - mocks.Shims.Setenv = func(key, value string) error { - return nil - } - - // Initialize with verbose context - verboseCtx := context.WithValue(context.Background(), "verbose", true) - err := pipeline.Initialize(mocks.Injector, verboseCtx) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - - // When Execute is called - err = pipeline.Execute(context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("SkipsVirtualMachineWhenNotColima", func(t *testing.T) { - // Given a pipeline with non-colima VM driver - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.InitializeFunc = func() error { return nil } - mockConfigHandler.IsLoadedFunc = func() bool { return true } - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - switch key { - case "vm.driver": - return "docker" // Not colima - default: - return "" - } - } - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - switch key { - case "docker.enabled": - return true - case "dns.enabled": - return false - default: - return false - } - } - - setupOptions := &SetupOptions{ConfigHandler: mockConfigHandler} - pipeline, mocks := setup(t, setupOptions) - - // Setup shims to allow NO_CACHE environment variable setting - mocks.Shims.Setenv = func(key, value string) error { - return nil - } - - vmUpCalled := false - mocks.VirtualMachine.UpFunc = func(verbose ...bool) error { - vmUpCalled = true - return nil - } - - // When Execute is called - err := pipeline.Execute(context.Background()) - - // Then no error should be returned and VM Up should not be called - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if vmUpCalled { - t.Error("Expected virtual machine Up to not be called when driver is not colima") - } - }) - - t.Run("SkipsContainerRuntimeWhenDisabled", func(t *testing.T) { - // Given a pipeline with docker disabled - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.InitializeFunc = func() error { return nil } - mockConfigHandler.IsLoadedFunc = func() bool { return true } - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - switch key { - case "vm.driver": - return "colima" - default: - return "" - } - } - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - switch key { - case "docker.enabled": - return false // Docker disabled - case "dns.enabled": - return false - default: - return false - } - } - - setupOptions := &SetupOptions{ConfigHandler: mockConfigHandler} - pipeline, mocks := setup(t, setupOptions) - - // Setup shims to allow NO_CACHE environment variable setting - mocks.Shims.Setenv = func(key, value string) error { - return nil - } - - containerUpCalled := false - mocks.ContainerRuntime.UpFunc = func(verbose ...bool) error { - containerUpCalled = true - return nil - } - - // When Execute is called - err := pipeline.Execute(context.Background()) - - // Then no error should be returned and container runtime Up should not be called - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if containerUpCalled { - t.Error("Expected container runtime Up to not be called when docker is disabled") - } - }) - - t.Run("ShowsDNSNotificationWhenEnabled", func(t *testing.T) { - // Given a pipeline with DNS enabled - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.InitializeFunc = func() error { return nil } - mockConfigHandler.IsLoadedFunc = func() bool { return true } - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - return "" - } - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - switch key { - case "docker.enabled": - return true - case "dns.enabled": - return true // DNS enabled - default: - return false - } - } - - setupOptions := &SetupOptions{ConfigHandler: mockConfigHandler} - pipeline, mocks := setup(t, setupOptions) - - // Setup shims to allow NO_CACHE environment variable setting - mocks.Shims.Setenv = func(key, value string) error { - return nil - } - - // When Execute is called - err := pipeline.Execute(context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // The notification appears in stderr output (visible in test output) - }) - - t.Run("SkipsDNSNotificationWhenDisabled", func(t *testing.T) { - // Given a pipeline with DNS disabled - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.InitializeFunc = func() error { return nil } - mockConfigHandler.IsLoadedFunc = func() bool { return true } - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - return "" - } - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - switch key { - case "docker.enabled": - return true - case "dns.enabled": - return false // DNS disabled - default: - return false - } - } - - setupOptions := &SetupOptions{ConfigHandler: mockConfigHandler} - pipeline, mocks := setup(t, setupOptions) - - // Setup shims to allow NO_CACHE environment variable setting - mocks.Shims.Setenv = func(key, value string) error { - return nil - } - - // When Execute is called - err := pipeline.Execute(context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // No notification should appear (can verify by comparing test output) - }) - - // Test execution failures - execFailureTests := []struct { - name string - setupMock func(*UpMocks) - expectedErr string - }{ - { - name: "ReturnsErrorWhenSetenvFails", - setupMock: func(mocks *UpMocks) { - mocks.Shims.Setenv = func(key, value string) error { - return fmt.Errorf("setenv failed") - } - }, - expectedErr: "Error setting NO_CACHE environment variable: setenv failed", - }, - { - name: "ReturnsErrorWhenToolsCheckFails", - setupMock: func(mocks *UpMocks) { - mocks.Shims.Setenv = func(key, value string) error { return nil } - mocks.ToolsManager.CheckFunc = func() error { - return fmt.Errorf("tools check failed") - } - }, - expectedErr: "Error checking tools: tools check failed", - }, - { - name: "ReturnsErrorWhenToolsInstallFails", - setupMock: func(mocks *UpMocks) { - mocks.Shims.Setenv = func(key, value string) error { return nil } - mocks.ToolsManager.InstallFunc = func() error { - return fmt.Errorf("tools install failed") - } - }, - expectedErr: "Error installing tools: tools install failed", - }, - { - name: "ReturnsErrorWhenVirtualMachineUpFails", - setupMock: func(mocks *UpMocks) { - mocks.Shims.Setenv = func(key, value string) error { return nil } - mocks.VirtualMachine.UpFunc = func(verbose ...bool) error { - return fmt.Errorf("vm up failed") - } - }, - expectedErr: "Error running virtual machine Up command: vm up failed", - }, - { - name: "ReturnsErrorWhenContainerRuntimeUpFails", - setupMock: func(mocks *UpMocks) { - mocks.Shims.Setenv = func(key, value string) error { return nil } - mocks.ContainerRuntime.UpFunc = func(verbose ...bool) error { - return fmt.Errorf("container runtime up failed") - } - }, - expectedErr: "Error running container runtime Up command: container runtime up failed", - }, - { - name: "ReturnsErrorWhenNetworkConfigureGuestFails", - setupMock: func(mocks *UpMocks) { - mocks.Shims.Setenv = func(key, value string) error { return nil } - mocks.NetworkManager.ConfigureGuestFunc = func() error { - return fmt.Errorf("configure guest failed") - } - }, - expectedErr: "Error configuring guest network: configure guest failed", - }, - { - name: "ReturnsErrorWhenNetworkConfigureHostRouteFails", - setupMock: func(mocks *UpMocks) { - mocks.Shims.Setenv = func(key, value string) error { return nil } - mocks.NetworkManager.ConfigureHostRouteFunc = func() error { - return fmt.Errorf("configure host route failed") - } - }, - expectedErr: "Error configuring host network: configure host route failed", - }, - { - name: "ReturnsErrorWhenNetworkConfigureDNSFails", - setupMock: func(mocks *UpMocks) { - mocks.Shims.Setenv = func(key, value string) error { return nil } - mocks.NetworkManager.ConfigureDNSFunc = func() error { - return fmt.Errorf("configure dns failed") - } - }, - expectedErr: "Error configuring DNS: configure dns failed", - }, - { - name: "ReturnsErrorWhenStackUpFails", - setupMock: func(mocks *UpMocks) { - mocks.Shims.Setenv = func(key, value string) error { return nil } - mocks.Stack.UpFunc = func(blueprint *blueprintv1alpha1.Blueprint) error { - return fmt.Errorf("stack up failed") - } - }, - expectedErr: "Error running stack Up command: stack up failed", - }, - } - - for _, tt := range execFailureTests { - t.Run(tt.name, func(t *testing.T) { - // Given an up pipeline with failing component - pipeline, mocks := setup(t) - tt.setupMock(mocks) - - ctx := context.Background() - - // When executing the pipeline - err := pipeline.Execute(ctx) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != tt.expectedErr { - t.Errorf("Expected error %q, got %q", tt.expectedErr, err.Error()) - } - }) - } - - t.Run("ReturnsErrorWhenNoVirtualMachineFound", func(t *testing.T) { - // Given an up pipeline with nil virtual machine - pipeline, mocks := setup(t) - - // Setup shims to allow NO_CACHE environment variable setting - mocks.Shims.Setenv = func(key, value string) error { return nil } - - // Set virtual machine to nil - pipeline.virtualMachine = nil - - // When executing the pipeline - err := pipeline.Execute(context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "No virtual machine found" { - t.Errorf("Expected 'No virtual machine found', got %q", err.Error()) - } - }) - - t.Run("ReturnsErrorWhenNoContainerRuntimeFound", func(t *testing.T) { - // Given an up pipeline with nil container runtime - pipeline, mocks := setup(t) - - // Setup shims to allow NO_CACHE environment variable setting - mocks.Shims.Setenv = func(key, value string) error { return nil } - - // Set container runtime to nil - pipeline.containerRuntime = nil - - // When executing the pipeline - err := pipeline.Execute(context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "No container runtime found" { - t.Errorf("Expected 'No container runtime found', got %q", err.Error()) - } - }) - - t.Run("ReturnsErrorWhenNoNetworkManagerFound", func(t *testing.T) { - // Given an up pipeline with nil network manager - pipeline, mocks := setup(t) - - // Setup shims to allow NO_CACHE environment variable setting - mocks.Shims.Setenv = func(key, value string) error { return nil } - - // Set network manager to nil - pipeline.networkManager = nil - - // When executing the pipeline - err := pipeline.Execute(context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "No network manager found" { - t.Errorf("Expected 'No network manager found', got %q", err.Error()) - } - }) - - t.Run("ReturnsErrorWhenNoStackFound", func(t *testing.T) { - // Given an up pipeline with nil stack - pipeline, mocks := setup(t) - - // Setup shims to allow NO_CACHE environment variable setting - mocks.Shims.Setenv = func(key, value string) error { return nil } - - // Set stack to nil - pipeline.stack = nil - - // When executing the pipeline - err := pipeline.Execute(context.Background()) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "No stack found" { - t.Errorf("Expected 'No stack found', got %q", err.Error()) - } - }) -} diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go deleted file mode 100644 index ca2d79d78..000000000 --- a/pkg/runtime/runtime.go +++ /dev/null @@ -1,495 +0,0 @@ -package runtime - -import ( - "fmt" - "maps" - "os" - - "github.com/windsorcli/cli/pkg/composer/artifact" - "github.com/windsorcli/cli/pkg/composer/blueprint" - "github.com/windsorcli/cli/pkg/composer/terraform" - "github.com/windsorcli/cli/pkg/context" - "github.com/windsorcli/cli/pkg/context/config" - envvars "github.com/windsorcli/cli/pkg/context/env" - "github.com/windsorcli/cli/pkg/context/secrets" - "github.com/windsorcli/cli/pkg/context/shell" - "github.com/windsorcli/cli/pkg/context/shell/ssh" - "github.com/windsorcli/cli/pkg/context/tools" - "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/generators" - "github.com/windsorcli/cli/pkg/provisioner/cluster" - "github.com/windsorcli/cli/pkg/provisioner/kubernetes" - "github.com/windsorcli/cli/pkg/workstation" - "github.com/windsorcli/cli/pkg/workstation/network" - "github.com/windsorcli/cli/pkg/workstation/services" - "github.com/windsorcli/cli/pkg/workstation/virt" -) - -// ============================================================================= -// Types -// ============================================================================= - -// Dependencies contains all the dependencies that Runtime might need. -// This allows for explicit dependency injection without complex DI frameworks. -type Dependencies struct { - Injector di.Injector - Shell shell.Shell - ConfigHandler config.ConfigHandler - ToolsManager tools.ToolsManager - EnvPrinters struct { - AwsEnv envvars.EnvPrinter - AzureEnv envvars.EnvPrinter - DockerEnv envvars.EnvPrinter - KubeEnv envvars.EnvPrinter - TalosEnv envvars.EnvPrinter - TerraformEnv envvars.EnvPrinter - WindsorEnv envvars.EnvPrinter - } - SecretsProviders struct { - Sops secrets.SecretsProvider - Onepassword secrets.SecretsProvider - } - BlueprintHandler blueprint.BlueprintHandler - ArtifactBuilder artifact.Artifact - Generators struct { - GitGenerator generators.Generator - TerraformGenerator generators.Generator - } - TerraformResolver terraform.ModuleResolver - ClusterClient cluster.ClusterClient - K8sManager kubernetes.KubernetesManager - Workstation struct { - Virt virt.Virt - Services struct { - DnsService services.Service - GitLivereloadService services.Service - LocalstackService services.Service - RegistryServices map[string]services.Service - TalosServices map[string]services.Service - } - Network network.NetworkManager - Ssh ssh.SSHClient - } -} - -// EnvVarsOptions contains options for environment variable operations. -type EnvVarsOptions struct { - Decrypt bool // Whether to decrypt secrets - Verbose bool // Whether to show verbose error output - Export bool // Whether to use export format (export KEY=value vs KEY=value) - OutputFunc func(string) // Callback function for handling output -} - -// Runtime encapsulates all core Windsor CLI runtime dependencies. -type Runtime struct { - Dependencies - Shims *Shims - EnvVars map[string]string - EnvAliases map[string]string - err error -} - -// ============================================================================= -// Constructor -// ============================================================================= - -// NewRuntime creates a new Runtime instance with the provided dependencies. -func NewRuntime(deps ...*Dependencies) *Runtime { - var depsVal *Dependencies - if len(deps) > 0 && deps[0] != nil { - depsVal = deps[0] - } else { - depsVal = &Dependencies{} - } - if depsVal.Injector == nil { - depsVal.Injector = di.NewInjector() - } - return &Runtime{ - Dependencies: *depsVal, - Shims: NewShims(), - } -} - -// ============================================================================= -// Public Methods -// ============================================================================= - -// Do returns the cumulative error state from all preceding runtime operations. -func (r *Runtime) Do() error { - return r.err -} - -// InstallHook installs a shell hook for the specified shell type. -func (r *Runtime) InstallHook(shellType string) *Runtime { - if r.err != nil { - return r - } - if r.Shell == nil { - r.err = fmt.Errorf("shell not loaded - call LoadShell() first") - return r - } - r.err = r.Shell.InstallHook(shellType) - return r -} - -// SetContext sets the context for the configuration handler. -func (r *Runtime) SetContext(context string) *Runtime { - if r.err != nil { - return r - } - if r.ConfigHandler == nil { - r.err = fmt.Errorf("config handler not loaded - call LoadConfig() first") - return r - } - r.err = r.ConfigHandler.SetContext(context) - return r -} - -// PrintContext outputs the current context using the provided output function. -func (r *Runtime) PrintContext(outputFunc func(string)) *Runtime { - if r.err != nil { - return r - } - if r.ConfigHandler == nil { - r.err = fmt.Errorf("config handler not loaded - call LoadConfig() first") - return r - } - context := r.ConfigHandler.GetContext() - outputFunc(context) - return r -} - -// WriteResetToken writes a session/token reset file using the shell. -func (r *Runtime) WriteResetToken() *Runtime { - if r.err != nil { - return r - } - if r.Shell == nil { - r.err = fmt.Errorf("shell not loaded - call LoadShell() first") - return r - } - _, r.err = r.Shell.WriteResetToken() - return r -} - -// HandleSessionReset resets managed environment variables if needed before loading new ones. -// It checks for reset flags, session tokens, and context changes. Errors are recorded in r.err. -func (r *Runtime) HandleSessionReset() *Runtime { - if r.err != nil { - return r - } - if r.Shell == nil { - r.err = fmt.Errorf("shell not loaded - call LoadShell() first") - return r - } - - hasSessionToken := os.Getenv("WINDSOR_SESSION_TOKEN") != "" - shouldReset, err := r.Shell.CheckResetFlags() - if err != nil { - r.err = fmt.Errorf("failed to check reset flags: %w", err) - return r - } - if !hasSessionToken { - shouldReset = true - } - - if shouldReset { - r.Shell.Reset() - if err := os.Setenv("NO_CACHE", "true"); err != nil { - r.err = fmt.Errorf("failed to set NO_CACHE: %w", err) - return r - } - } - - return r -} - -// PrintEnvVars renders and prints the environment variables that were previously collected -// and stored in r.EnvVars using the shell's RenderEnvVars method. The EnvVarsOptions parameter -// controls export formatting and provides an output callback. This method should be called -// after LoadEnvVars to print the collected environment variables. -func (r *Runtime) PrintEnvVars(opts EnvVarsOptions) *Runtime { - if r.err != nil { - return r - } - - if len(r.EnvVars) > 0 { - output := r.Shell.RenderEnvVars(r.EnvVars, opts.Export) - opts.OutputFunc(output) - } - - return r -} - -// PrintAliases prints all collected aliases using the shell's RenderAliases method. -// The outputFunc callback is invoked with the rendered aliases string output. -// If any error occurs during alias retrieval, the Runtime error state is updated -// and the original instance is returned unmodified. -func (r *Runtime) PrintAliases(outputFunc func(string)) *Runtime { - if r.err != nil { - return r - } - - allAliases := make(map[string]string) - for _, envPrinter := range r.getAllEnvPrinters() { - aliases, err := envPrinter.GetAlias() - if err != nil { - r.err = fmt.Errorf("error getting aliases: %w", err) - return r - } - maps.Copy(allAliases, aliases) - } - - if len(allAliases) > 0 { - output := r.Shell.RenderAliases(allAliases) - outputFunc(output) - } - - return r -} - -// ExecutePostEnvHook executes post-environment hooks for all environment printers. -// The Verbose flag controls whether errors are reported. Returns the Runtime instance -// with error state updated if any step fails. -func (r *Runtime) ExecutePostEnvHook(verbose bool) *Runtime { - if r.err != nil { - return r - } - - var firstError error - - printers := r.getAllEnvPrinters() - - for _, printer := range printers { - if printer != nil { - if err := printer.PostEnvHook(); err != nil && firstError == nil { - firstError = err - } - } - } - - if firstError != nil && verbose { - r.err = fmt.Errorf("failed to execute post env hooks: %w", firstError) - return r - } - - return r -} - -// CheckTrustedDirectory checks if the current directory is trusted using the shell's -// CheckTrustedDirectory method. Returns the Runtime instance with updated error state. -func (r *Runtime) CheckTrustedDirectory() *Runtime { - if r.err != nil { - return r - } - if r.Shell == nil { - r.err = fmt.Errorf("shell not loaded - call LoadShell() first") - return r - } - - if err := r.Shell.CheckTrustedDirectory(); err != nil { - r.err = fmt.Errorf("not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve") - return r - } - - return r -} - -// ArtifactOptions contains options for artifact operations (bundle or push). -type ArtifactOptions struct { - // Bundle options - OutputPath string // Output path for bundle (file or directory) - - // Push options - RegistryBase string // Registry base URL (e.g., "ghcr.io") - RepoName string // Repository name - - // Common options - Tag string // Tag/version (overrides metadata.yaml) - OutputFunc func(string) // Callback for success output -} - -// ProcessArtifacts builds and processes artifacts (bundle or push) from the project's templates, -// kustomize, and terraform files. It loads blueprint and artifact handlers, bundles all files, -// and either archives to a file or pushes to a registry based on ArtifactOptions. Supports both -// bundle and push operations. If any step fails, the returned Runtime has an updated error state; -// otherwise, returns the current instance. -func (r *Runtime) ProcessArtifacts(opts ArtifactOptions) *Runtime { - if r.err != nil { - return r - } - if r.Shell == nil { - r.err = fmt.Errorf("shell not loaded - call LoadShell() first") - return r - } - - if r.ArtifactBuilder == nil { - if existingArtifactBuilder := r.Injector.Resolve("artifactBuilder"); existingArtifactBuilder != nil { - if artifactBuilderInstance, ok := existingArtifactBuilder.(artifact.Artifact); ok { - r.ArtifactBuilder = artifactBuilderInstance - } else { - r.ArtifactBuilder = artifact.NewArtifactBuilder() - r.Injector.Register("artifactBuilder", r.ArtifactBuilder) - } - } else { - r.ArtifactBuilder = artifact.NewArtifactBuilder() - r.Injector.Register("artifactBuilder", r.ArtifactBuilder) - } - if err := r.ArtifactBuilder.Initialize(r.Injector); err != nil { - r.err = fmt.Errorf("failed to initialize artifact builder: %w", err) - return r - } - } - - if opts.RegistryBase != "" && opts.RepoName != "" { - if err := r.ArtifactBuilder.Bundle(); err != nil { - r.err = fmt.Errorf("failed to bundle artifacts: %w", err) - return r - } - - if err := r.ArtifactBuilder.Push(opts.RegistryBase, opts.RepoName, opts.Tag); err != nil { - r.err = fmt.Errorf("failed to push artifact: %w", err) - return r - } - registryURL := fmt.Sprintf("%s/%s", opts.RegistryBase, opts.RepoName) - if opts.Tag != "" { - registryURL = fmt.Sprintf("%s:%s", registryURL, opts.Tag) - } - if opts.OutputFunc != nil { - opts.OutputFunc(registryURL) - } - } else { - actualOutputPath, err := r.ArtifactBuilder.Write(opts.OutputPath, opts.Tag) - if err != nil { - r.err = fmt.Errorf("failed to bundle and create artifact: %w", err) - return r - } - if opts.OutputFunc != nil { - opts.OutputFunc(actualOutputPath) - } - } - - return r -} - -// WorkstationUp starts the workstation environment, including VMs, containers, networking, and services. -// It returns the Runtime instance, propagating any errors encountered during workstation initialization or startup. -// This method should be called after configuration, shell, and dependencies are properly loaded. -func (r *Runtime) WorkstationUp() *Runtime { - if r.err != nil { - return r - } - ws, err := r.createWorkstation() - if err != nil { - r.err = err - return r - } - if err := ws.Up(); err != nil { - r.err = fmt.Errorf("failed to start workstation: %w", err) - return r - } - return r -} - -// WorkstationDown stops the workstation environment, ensuring all services, containers, VMs, and associated networking are gracefully shut down. -// It returns the Runtime instance, propagating any errors encountered during the stopping process. -// This method should be invoked after workstation operations are complete and a teardown is required. -func (r *Runtime) WorkstationDown() *Runtime { - if r.err != nil { - return r - } - - ws, err := r.createWorkstation() - if err != nil { - r.err = err - return r - } - - if err := ws.Down(); err != nil { - r.err = fmt.Errorf("failed to stop workstation: %w", err) - return r - } - - return r -} - -// ============================================================================= -// Private Methods -// ============================================================================= - -// createWorkstation creates and initializes a workstation instance with the correct execution context. -// It validates that all required dependencies (ConfigHandler, Shell, Injector) are loaded, retrieves the current context, -// obtains the project root, and assembles an ExecutionContext for workstation operations. It returns a newly created -// workstation.Workstation or an error if any setup step fails. This method is used internally by both WorkstationUp and WorkstationDown. -func (r *Runtime) createWorkstation() (*workstation.Workstation, error) { - if r.ConfigHandler == nil { - return nil, fmt.Errorf("config handler not loaded - call LoadConfig() first") - } - if r.Shell == nil { - return nil, fmt.Errorf("shell not loaded - call LoadShell() first") - } - if r.Injector == nil { - return nil, fmt.Errorf("injector not available") - } - - contextName := r.ConfigHandler.GetContext() - if contextName == "" { - return nil, fmt.Errorf("no context set - call SetContext() first") - } - - projectRoot, err := r.Shell.GetProjectRoot() - if err != nil { - return nil, fmt.Errorf("failed to get project root: %w", err) - } - - execCtx := &context.ExecutionContext{ - ContextName: contextName, - ProjectRoot: projectRoot, - ConfigRoot: fmt.Sprintf("%s/contexts/%s", projectRoot, contextName), - TemplateRoot: fmt.Sprintf("%s/contexts/_template", projectRoot), - ConfigHandler: r.ConfigHandler, - Shell: r.Shell, - Injector: r.Injector, - } - - workstationCtx := &workstation.WorkstationExecutionContext{ - ExecutionContext: *execCtx, - } - ws, err := workstation.NewWorkstation(workstationCtx, r.Injector) - if err != nil { - return nil, fmt.Errorf("failed to create workstation: %w", err) - } - - return ws, nil -} - -// getAllEnvPrinters returns all environment printers in field order, ensuring WindsorEnv is last. -// This method provides compile-time structure assertions by mirroring the struct layout definition. -// Panics at runtime if WindsorEnv is not last to guarantee environment variable precedence. -func (r *Runtime) getAllEnvPrinters() []envvars.EnvPrinter { - const expectedPrinterCount = 7 - _ = [expectedPrinterCount]struct{}{} - - allPrinters := []envvars.EnvPrinter{ - r.EnvPrinters.AwsEnv, - r.EnvPrinters.AzureEnv, - r.EnvPrinters.DockerEnv, - r.EnvPrinters.KubeEnv, - r.EnvPrinters.TalosEnv, - r.EnvPrinters.TerraformEnv, - r.EnvPrinters.WindsorEnv, - } - - var printers []envvars.EnvPrinter - for _, printer := range allPrinters { - if printer != nil { - printers = append(printers, printer) - } - } - - if len(printers) > 0 && printers[len(printers)-1] != r.EnvPrinters.WindsorEnv { - panic("WindsorEnv must be the last printer in the list") - } - - return printers -} diff --git a/pkg/runtime/runtime_loaders.go b/pkg/runtime/runtime_loaders.go deleted file mode 100644 index 29bc09724..000000000 --- a/pkg/runtime/runtime_loaders.go +++ /dev/null @@ -1,310 +0,0 @@ -package runtime - -import ( - "fmt" - "maps" - "os" - "path/filepath" - - secretsConfigType "github.com/windsorcli/cli/api/v1alpha1/secrets" - "github.com/windsorcli/cli/pkg/composer/blueprint" - "github.com/windsorcli/cli/pkg/context/config" - envvars "github.com/windsorcli/cli/pkg/context/env" - "github.com/windsorcli/cli/pkg/context/secrets" - "github.com/windsorcli/cli/pkg/context/shell" - "github.com/windsorcli/cli/pkg/provisioner/cluster" - "github.com/windsorcli/cli/pkg/provisioner/kubernetes" - k8sclient "github.com/windsorcli/cli/pkg/provisioner/kubernetes/client" -) - -// ============================================================================= -// Loader Methods -// ============================================================================= - -// LoadShell loads the shell dependency, creating a new default shell if none exists. -func (r *Runtime) LoadShell() *Runtime { - if r.err != nil { - return r - } - - if r.Shell == nil { - if existingShell := r.Injector.Resolve("shell"); existingShell != nil { - if shellInstance, ok := existingShell.(shell.Shell); ok { - r.Shell = shellInstance - } else { - r.Shell = shell.NewDefaultShell(r.Injector) - } - } else { - r.Shell = shell.NewDefaultShell(r.Injector) - } - } - r.Injector.Register("shell", r.Shell) - return r -} - -// LoadConfig initializes the configuration handler dependency and loads all configuration sources -// into memory for use by the runtime. This includes creating a new ConfigHandler if one does not exist, -// initializing it with required dependencies, and loading configuration from schema defaults, the root -// windsor.yaml context section, context-specific windsor.yaml/yml files, and values.yaml. The method -// registers the ConfigHandler with the injector, and returns the Runtime instance with any error set if -// initialization or configuration loading fails. This method supersedes the previous LoadConfigHandler by -// combining handler instantiation, initialization, and configuration loading in one step. -func (r *Runtime) LoadConfig() *Runtime { - if r.err != nil { - return r - } - if r.Shell == nil { - r.err = fmt.Errorf("shell not loaded - call LoadShell() first") - return r - } - - if r.ConfigHandler == nil { - if existingConfigHandler := r.Injector.Resolve("configHandler"); existingConfigHandler != nil { - if configHandlerInstance, ok := existingConfigHandler.(config.ConfigHandler); ok { - r.ConfigHandler = configHandlerInstance - } else { - r.ConfigHandler = config.NewConfigHandler(r.Injector) - } - } else { - r.ConfigHandler = config.NewConfigHandler(r.Injector) - } - } - r.Injector.Register("configHandler", r.ConfigHandler) - if err := r.ConfigHandler.Initialize(); err != nil { - r.err = fmt.Errorf("failed to initialize config handler: %w", err) - return r - } - if err := r.ConfigHandler.LoadConfig(); err != nil { - r.err = fmt.Errorf("failed to load configuration: %w", err) - return r - } - return r -} - -// LoadSecretsProviders loads and initializes the secrets providers using configuration and environment. -// It detects SOPS and 1Password vaults as in BasePipeline.withSecretsProviders. -func (r *Runtime) LoadSecretsProviders() *Runtime { - if r.err != nil { - return r - } - if r.ConfigHandler == nil { - r.err = fmt.Errorf("config handler not loaded - call LoadConfig() first") - return r - } - - configRoot, err := r.ConfigHandler.GetConfigRoot() - if err != nil { - r.err = fmt.Errorf("error getting config root: %w", err) - return r - } - - secretsFilePaths := []string{"secrets.enc.yaml", "secrets.enc.yml"} - for _, filePath := range secretsFilePaths { - if _, err := r.Shims.Stat(filepath.Join(configRoot, filePath)); err == nil { - if r.SecretsProviders.Sops == nil { - r.SecretsProviders.Sops = secrets.NewSopsSecretsProvider(configRoot, r.Injector) - r.Injector.Register("sopsSecretsProvider", r.SecretsProviders.Sops) - } - break - } - } - - vaults, ok := r.ConfigHandler.Get("secrets.onepassword.vaults").(map[string]secretsConfigType.OnePasswordVault) - if ok && len(vaults) > 0 { - useSDK := r.Shims.Getenv("OP_SERVICE_ACCOUNT_TOKEN") != "" - - for key, vault := range vaults { - vaultCopy := vault - vaultCopy.ID = key - - if r.SecretsProviders.Onepassword == nil { - if useSDK { - r.SecretsProviders.Onepassword = secrets.NewOnePasswordSDKSecretsProvider(vaultCopy, r.Injector) - } else { - r.SecretsProviders.Onepassword = secrets.NewOnePasswordCLISecretsProvider(vaultCopy, r.Injector) - } - r.Injector.Register("onePasswordSecretsProvider", r.SecretsProviders.Onepassword) - break - } - } - } - - return r -} - -// LoadKubernetes loads and initializes Kubernetes and cluster client dependencies. -func (r *Runtime) LoadKubernetes() *Runtime { - if r.err != nil { - return r - } - if r.ConfigHandler == nil { - r.err = fmt.Errorf("config handler not loaded - call LoadConfig() first") - return r - } - - driver := r.ConfigHandler.GetString("cluster.driver") - if driver != "" && driver != "talos" { - r.err = fmt.Errorf("unsupported cluster driver: %s", driver) - return r - } - - if r.Injector.Resolve("kubernetesClient") == nil { - kubernetesClient := k8sclient.NewDynamicKubernetesClient() - r.Injector.Register("kubernetesClient", kubernetesClient) - } - - 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 - } - - if driver == "talos" { - if r.ClusterClient == nil { - r.ClusterClient = cluster.NewTalosClusterClient(r.Injector) - r.Injector.Register("clusterClient", r.ClusterClient) - } - } - - return r -} - -// LoadBlueprint initializes and configures the blueprint handler for template processing and blueprint data management. -// It creates and registers the blueprint handler if it does not already exist, then initializes it and loads blueprint data. -// All dependencies are injected and registered as needed. If any error occurs during initialization, the error is set -// in the runtime and the method returns. Returns the Runtime instance with updated dependencies and error state. -func (r *Runtime) LoadBlueprint() *Runtime { - if r.err != nil { - return r - } - if r.ConfigHandler == nil { - r.err = fmt.Errorf("config handler not loaded - call LoadConfig() first") - return r - } - if r.BlueprintHandler == nil { - r.BlueprintHandler = blueprint.NewBlueprintHandler(r.Injector) - r.Injector.Register("blueprintHandler", r.BlueprintHandler) - } - if err := r.BlueprintHandler.Initialize(); err != nil { - r.err = fmt.Errorf("failed to initialize blueprint handler: %w", err) - return r - } - if err := r.BlueprintHandler.LoadBlueprint(); err != nil { - r.err = fmt.Errorf("failed to load blueprint data: %w", err) - return r - } - return r -} - -// LoadEnvVars loads environment variables and injects them into the process environment. -// It initializes secret providers if Decrypt is true, and aggregates environment variables -// and aliases from all enabled environment printers. Environment variables are injected -// into the current process, and aliases are collected for later use. The Verbose flag controls -// whether secret loading errors are reported. Returns the Runtime instance with the error -// state updated if any failure occurs during processing. -func (r *Runtime) LoadEnvVars(opts EnvVarsOptions) *Runtime { - if r.err != nil { - return r - } - if r.ConfigHandler == nil { - r.err = fmt.Errorf("config handler not loaded - call LoadConfig() first") - return r - } - - if r.EnvPrinters.AwsEnv == nil && r.ConfigHandler.GetBool("aws.enabled", false) { - r.EnvPrinters.AwsEnv = envvars.NewAwsEnvPrinter(r.Injector) - r.Injector.Register("awsEnv", r.EnvPrinters.AwsEnv) - } - if r.EnvPrinters.AzureEnv == nil && r.ConfigHandler.GetBool("azure.enabled", false) { - r.EnvPrinters.AzureEnv = envvars.NewAzureEnvPrinter(r.Injector) - r.Injector.Register("azureEnv", r.EnvPrinters.AzureEnv) - } - if r.EnvPrinters.DockerEnv == nil && r.ConfigHandler.GetBool("docker.enabled", false) { - r.EnvPrinters.DockerEnv = envvars.NewDockerEnvPrinter(r.Injector) - r.Injector.Register("dockerEnv", r.EnvPrinters.DockerEnv) - } - if r.EnvPrinters.KubeEnv == nil && r.ConfigHandler.GetBool("cluster.enabled", false) { - r.EnvPrinters.KubeEnv = envvars.NewKubeEnvPrinter(r.Injector) - r.Injector.Register("kubeEnv", r.EnvPrinters.KubeEnv) - } - if r.EnvPrinters.TalosEnv == nil && - (r.ConfigHandler.GetString("cluster.driver", "") == "talos" || - r.ConfigHandler.GetString("cluster.driver", "") == "omni") { - r.EnvPrinters.TalosEnv = envvars.NewTalosEnvPrinter(r.Injector) - r.Injector.Register("talosEnv", r.EnvPrinters.TalosEnv) - } - if r.EnvPrinters.TerraformEnv == nil && r.ConfigHandler.GetBool("terraform.enabled", false) { - r.EnvPrinters.TerraformEnv = envvars.NewTerraformEnvPrinter(r.Injector) - r.Injector.Register("terraformEnv", r.EnvPrinters.TerraformEnv) - } - if r.EnvPrinters.WindsorEnv == nil { - r.EnvPrinters.WindsorEnv = envvars.NewWindsorEnvPrinter(r.Injector) - r.Injector.Register("windsorEnv", r.EnvPrinters.WindsorEnv) - } - - for _, printer := range r.getAllEnvPrinters() { - if printer != nil { - if err := printer.Initialize(); err != nil { - r.err = fmt.Errorf("failed to initialize env printer: %w", err) - return r - } - } - } - - if opts.Decrypt && (r.SecretsProviders.Sops != nil || r.SecretsProviders.Onepassword != nil) { - providers := []secrets.SecretsProvider{ - r.SecretsProviders.Sops, - r.SecretsProviders.Onepassword, - } - for _, provider := range providers { - if provider != nil { - if err := provider.LoadSecrets(); err != nil { - if opts.Verbose { - r.err = fmt.Errorf("failed to load secrets: %w", err) - return r - } - return r - } - } - } - } - - allEnvVars := make(map[string]string) - allAliases := make(map[string]string) - - printers := r.getAllEnvPrinters() - - for _, printer := range printers { - if printer != nil { - envVars, err := printer.GetEnvVars() - if err != nil { - r.err = fmt.Errorf("error getting environment variables: %w", err) - return r - } - maps.Copy(allEnvVars, envVars) - - aliases, err := printer.GetAlias() - if err != nil { - r.err = fmt.Errorf("error getting aliases: %w", err) - return r - } - maps.Copy(allAliases, aliases) - } - } - - r.EnvVars = allEnvVars - - for key, value := range allEnvVars { - if err := os.Setenv(key, value); err != nil { - r.err = fmt.Errorf("error setting environment variable %s: %w", key, err) - return r - } - } - - r.EnvAliases = allAliases - - return r -} diff --git a/pkg/runtime/runtime_loaders_test.go b/pkg/runtime/runtime_loaders_test.go deleted file mode 100644 index 57b993518..000000000 --- a/pkg/runtime/runtime_loaders_test.go +++ /dev/null @@ -1,857 +0,0 @@ -package runtime - -import ( - "errors" - "os" - "strings" - "testing" - - secretsConfigType "github.com/windsorcli/cli/api/v1alpha1/secrets" - "github.com/windsorcli/cli/pkg/context/config" - "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/provisioner/cluster" - "github.com/windsorcli/cli/pkg/provisioner/kubernetes" - k8sclient "github.com/windsorcli/cli/pkg/provisioner/kubernetes/client" - "github.com/windsorcli/cli/pkg/context/shell" -) - -// The RuntimeLoadersTest is a test suite for the Runtime loader methods. -// It provides comprehensive test coverage for dependency loading, error propagation, -// and method chaining in the Windsor CLI runtime system. -// The RuntimeLoadersTest acts as a validation framework for loader functionality, -// ensuring reliable dependency management, proper error handling, and method chaining. - -// ============================================================================= -// Test Setup -// ============================================================================= - -// setupMocks creates a new set of mocks for testing -func setupMocks(t *testing.T) *Dependencies { - t.Helper() - - return &Dependencies{ - Injector: di.NewInjector(), - Shell: shell.NewMockShell(), - ConfigHandler: config.NewMockConfigHandler(), - } -} - -// ============================================================================= -// Test Loader Methods -// ============================================================================= - -func TestRuntime_LoadShell(t *testing.T) { - t.Run("LoadsShellSuccessfully", func(t *testing.T) { - // Given a runtime with dependencies - mocks := setupMocks(t) - runtime := NewRuntime(mocks) - - // When loading shell - result := runtime.LoadShell() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadShell to return the same runtime instance") - } - - // And shell should be loaded - if runtime.Shell == nil { - t.Error("Expected shell to be loaded") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - }) - - t.Run("CreatesNewShellWhenNoneExists", func(t *testing.T) { - // Given a runtime without pre-loaded shell - runtime := NewRuntime() - - // When loading shell - result := runtime.LoadShell() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadShell to return the same runtime instance") - } - - // And shell should be loaded - if runtime.Shell == nil { - t.Error("Expected shell to be loaded") - } - - // And shell should be registered in injector - resolvedShell := runtime.Injector.Resolve("shell") - if resolvedShell == nil { - t.Error("Expected shell 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("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 shell - result := runtime.LoadShell() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadShell to return the same runtime instance") - } - - // And shell should not be loaded - if runtime.Shell != nil { - t.Error("Expected shell 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) - } - }) -} - -func TestRuntime_LoadConfig(t *testing.T) { - t.Run("LoadsConfigSuccessfully", func(t *testing.T) { - // Given a runtime with loaded shell - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell() - - // When loading config - result := runtime.LoadConfig() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadConfigHandler to return the same runtime instance") - } - - // And config should be loaded - if runtime.ConfigHandler == nil { - t.Error("Expected config to be loaded") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - }) - - t.Run("ReturnsErrorWhenShellNotLoaded", func(t *testing.T) { - // Given a runtime without loaded shell (no pre-loaded dependencies) - runtime := NewRuntime() - - // When loading config - result := runtime.LoadConfig() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadConfigHandler to return the same runtime instance") - } - - // And error should be set - if runtime.err == nil { - t.Error("Expected error when shell not loaded") - } - - expectedError := "shell not loaded - call LoadShell() first" - if runtime.err.Error() != expectedError { - t.Errorf("Expected error %q, got %q", expectedError, runtime.err.Error()) - } - }) - - t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { - // Given a runtime with an existing error (no pre-loaded dependencies) - runtime := NewRuntime() - runtime.err = errors.New("existing error") - - // When loading config - result := runtime.LoadConfig() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadConfigHandler to return the same runtime instance") - } - - // And config handler should not be loaded - if runtime.ConfigHandler != nil { - t.Error("Expected config handler 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("PropagatesConfigHandlerInitializationError", func(t *testing.T) { - // Given a runtime with an injector that cannot resolve the shell - runtime := NewRuntime() - runtime.Shell = shell.NewMockShell() - // Don't register the shell in the injector - this will cause initialization to fail - - // When loading config - result := runtime.LoadConfig() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadConfigHandler to return the same runtime instance") - } - - // And error should be set from initialization failure - if runtime.err == nil { - t.Error("Expected error from config handler initialization failure") - } else { - expectedError := "failed to initialize config handler" - if !strings.Contains(runtime.err.Error(), expectedError) { - t.Errorf("Expected error to contain %q, got %q", expectedError, runtime.err.Error()) - } - } - }) -} - -func TestRuntime_LoadSecretsProviders(t *testing.T) { - t.Run("LoadsSecretsProvidersSuccessfully", func(t *testing.T) { - // Given a runtime with loaded shell and config handler - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetConfigRootFunc = func() (string, error) { - return "/test/config/root", nil - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // When loading secrets providers - result := runtime.LoadSecretsProviders() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadSecretsProviders to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - }) - - t.Run("ReturnsErrorWhenConfigHandlerNotLoaded", func(t *testing.T) { - // Given a runtime without loaded config handler (no pre-loaded dependencies) - runtime := NewRuntime() - - // When loading secrets providers - result := runtime.LoadSecretsProviders() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadSecretsProviders to return the same runtime instance") - } - - // And error should be set - if runtime.err == nil { - t.Error("Expected error when config handler not loaded") - } - - expectedError := "config handler not loaded - call LoadConfig() first" - if runtime.err.Error() != expectedError { - t.Errorf("Expected error %q, got %q", expectedError, runtime.err.Error()) - } - }) - - t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { - // Given a runtime with an existing error (no pre-loaded dependencies) - runtime := NewRuntime() - runtime.err = errors.New("existing error") - - // When loading secrets providers - result := runtime.LoadSecretsProviders() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadSecretsProviders to return the same runtime instance") - } - - // And original error should be preserved - if runtime.err.Error() != "existing error" { - t.Errorf("Expected original error to be preserved, got %v", runtime.err) - } - - // And no secrets providers should be loaded - if runtime.SecretsProviders.Sops != nil { - t.Error("Expected no secrets providers to be loaded when error exists") - } - if runtime.SecretsProviders.Onepassword != nil { - t.Error("Expected no secrets providers to be loaded when error exists") - } - }) - - t.Run("PropagatesConfigRootError", func(t *testing.T) { - // Given a runtime with loaded shell and config handler that returns error for GetConfigRoot - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetConfigRootFunc = func() (string, error) { - return "", errors.New("config root error") - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // When loading secrets providers - result := runtime.LoadSecretsProviders() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadSecretsProviders to return the same runtime instance") - } - - // And error should be propagated - if runtime.err == nil { - t.Error("Expected error to be propagated from config root") - } else { - expectedError := "error getting config root" - if !strings.Contains(runtime.err.Error(), expectedError) { - t.Errorf("Expected error to contain %q, got %q", expectedError, runtime.err.Error()) - } - } - }) - - t.Run("LoadsSopsProviderWhenSecretsFileExists", func(t *testing.T) { - // Given a runtime with loaded shell and config handler, and secrets file exists - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetConfigRootFunc = func() (string, error) { - return "/test/config/root", nil - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // Mock Stat to return success for secrets.enc.yaml - runtime.Shims.Stat = func(name string) (os.FileInfo, error) { - if strings.Contains(name, "secrets.enc.yaml") { - return nil, nil // Success - file exists - } - return nil, errors.New("file not found") - } - - // When loading secrets providers - result := runtime.LoadSecretsProviders() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadSecretsProviders to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And Sops provider should be loaded - if runtime.SecretsProviders.Sops == nil { - t.Error("Expected Sops provider to be loaded when secrets file exists") - } - - // And Sops provider should be registered in injector - resolvedSops := runtime.Injector.Resolve("sopsSecretsProvider") - if resolvedSops == nil { - t.Error("Expected Sops provider to be registered in injector") - } - }) - - t.Run("LoadsOnePasswordSDKProviderWhenTokenExists", func(t *testing.T) { - // Given a runtime with loaded shell and config handler, OnePassword vaults configured, and SDK token - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetConfigRootFunc = func() (string, error) { - return "/test/config/root", nil - } - mocks.ConfigHandler.(*config.MockConfigHandler).GetFunc = func(key string) any { - if key == "secrets.onepassword.vaults" { - return map[string]secretsConfigType.OnePasswordVault{ - "vault1": {URL: "https://vault1.com", Name: "Vault 1"}, - } - } - return nil - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // Mock Getenv to return SDK token - runtime.Shims.Getenv = func(key string) string { - if key == "OP_SERVICE_ACCOUNT_TOKEN" { - return "test-token" - } - return "" - } - - // When loading secrets providers - result := runtime.LoadSecretsProviders() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadSecretsProviders to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And OnePassword provider should be loaded - if runtime.SecretsProviders.Onepassword == nil { - t.Error("Expected OnePassword provider to be loaded when vaults configured and SDK token exists") - } - - // And OnePassword provider should be registered in injector - resolvedOnePassword := runtime.Injector.Resolve("onePasswordSecretsProvider") - if resolvedOnePassword == nil { - t.Error("Expected OnePassword provider to be registered in injector") - } - }) - - t.Run("LoadsOnePasswordCLIProviderWhenNoToken", func(t *testing.T) { - // Given a runtime with loaded shell and config handler, OnePassword vaults configured, but no SDK token - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetConfigRootFunc = func() (string, error) { - return "/test/config/root", nil - } - mocks.ConfigHandler.(*config.MockConfigHandler).GetFunc = func(key string) any { - if key == "secrets.onepassword.vaults" { - return map[string]secretsConfigType.OnePasswordVault{ - "vault1": {URL: "https://vault1.com", Name: "Vault 1"}, - } - } - return nil - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // Mock Getenv to return no SDK token - runtime.Shims.Getenv = func(key string) string { - return "" - } - - // When loading secrets providers - result := runtime.LoadSecretsProviders() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadSecretsProviders to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And OnePassword provider should be loaded - if runtime.SecretsProviders.Onepassword == nil { - t.Error("Expected OnePassword provider to be loaded when vaults configured but no SDK token") - } - - // And OnePassword provider should be registered in injector - resolvedOnePassword := runtime.Injector.Resolve("onePasswordSecretsProvider") - if resolvedOnePassword == nil { - t.Error("Expected OnePassword provider to be registered in injector") - } - }) - - t.Run("DoesNotLoadProvidersWhenNoConfig", func(t *testing.T) { - // Given a runtime with loaded shell and config handler, but no secrets configuration - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetConfigRootFunc = func() (string, error) { - return "/test/config/root", nil - } - mocks.ConfigHandler.(*config.MockConfigHandler).GetFunc = func(key string) any { - return nil // No secrets configuration - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // Mock Stat to return file not found for secrets files - runtime.Shims.Stat = func(name string) (os.FileInfo, error) { - return nil, errors.New("file not found") - } - - // Mock Getenv to return no SDK token - runtime.Shims.Getenv = func(key string) string { - return "" - } - - // When loading secrets providers - result := runtime.LoadSecretsProviders() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadSecretsProviders to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And no secrets providers should be loaded - if runtime.SecretsProviders.Sops != nil { - t.Error("Expected Sops provider to not be loaded when no secrets file exists") - } - if runtime.SecretsProviders.Onepassword != nil { - t.Error("Expected OnePassword provider to not be loaded when no vaults configured") - } - }) -} - -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().LoadConfig() - - // 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 LoadConfig() 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().LoadConfig() - - // 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().LoadConfig() - - // 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 := k8sclient.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().LoadConfig() - - // 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().LoadConfig() - - // 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("LoadsKubernetesWithEmptyClusterDriver", func(t *testing.T) { - // Given a runtime with loaded config handler - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // And mock config handler returns empty string for cluster driver (generic k8s) - mockConfigHandler := runtime.ConfigHandler.(*config.MockConfigHandler) - mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { - if key == "cluster.driver" { - return "" - } - return "mock-string" - } - - // When loading kubernetes - result := runtime.LoadKubernetes() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadKubernetes to return the same runtime instance") - } - - // And kubernetes client should be registered in injector - kubernetesClient := runtime.Injector.Resolve("kubernetesClient") - if kubernetesClient == nil { - t.Error("Expected kubernetes client to be registered in injector") - } - - // And kubernetes manager should be loaded - if runtime.K8sManager == nil { - t.Error("Expected kubernetes manager to be loaded") - } - - // And kubernetes manager should be registered in injector - kubernetesManager := runtime.Injector.Resolve("kubernetesManager") - if kubernetesManager == nil { - t.Error("Expected kubernetes manager to be registered in injector") - } - - // And cluster client should NOT be loaded (no specific cluster driver) - if runtime.ClusterClient != nil { - t.Error("Expected cluster client to not be loaded when no cluster driver specified") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - }) - - t.Run("PropagatesKubernetesManagerInitializationError", func(t *testing.T) { - // Given a runtime with loaded config handler - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // 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()) - } - }) -} diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go deleted file mode 100644 index ef939cf2d..000000000 --- a/pkg/runtime/runtime_test.go +++ /dev/null @@ -1,2227 +0,0 @@ -package runtime - -import ( - "errors" - "os" - "path/filepath" - "reflect" - "strings" - "testing" - - "github.com/windsorcli/cli/pkg/context/config" - envvars "github.com/windsorcli/cli/pkg/context/env" - "github.com/windsorcli/cli/pkg/context/shell" -) - -// The RuntimeTest is a test suite for the Runtime struct and its chaining methods. -// It provides comprehensive test coverage for dependency loading, error propagation, -// and command execution in the Windsor CLI runtime system. -// The RuntimeTest acts as a validation framework for runtime functionality, -// ensuring reliable dependency management, proper error handling, and method chaining. - -// ============================================================================= -// Test Setup -// ============================================================================= - -// ============================================================================= -// Test Public Methods -// ============================================================================= - -func TestRuntime_NewRuntime(t *testing.T) { - t.Run("CreatesRuntimeWithDependencies", func(t *testing.T) { - // Given dependencies - mocks := setupMocks(t) - - // When creating a new runtime - runtime := NewRuntime(mocks) - - // Then runtime should be created successfully - if runtime == nil { - t.Error("Expected runtime to be created") - } - - if runtime.Injector != mocks.Injector { - t.Error("Expected injector to be set") - } - }) -} - -func TestRuntime_InstallHook(t *testing.T) { - t.Run("InstallsHookSuccessfully", func(t *testing.T) { - // Given a runtime with loaded shell - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell() - - // When installing hook - result := runtime.InstallHook("bash") - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected InstallHook to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - }) - - t.Run("ReturnsErrorWhenShellNotLoaded", func(t *testing.T) { - // Given a runtime without loaded shell (no pre-loaded dependencies) - runtime := NewRuntime() - - // When installing hook - result := runtime.InstallHook("bash") - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected InstallHook to return the same runtime instance") - } - - // And error should be set - if runtime.err == nil { - t.Error("Expected error when shell not loaded") - } - - expectedError := "shell not loaded - call LoadShell() first" - if runtime.err.Error() != expectedError { - t.Errorf("Expected error %q, got %q", expectedError, runtime.err.Error()) - } - }) - - t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { - // Given a runtime with an existing error (no pre-loaded dependencies) - runtime := NewRuntime() - runtime.err = errors.New("existing error") - - // When installing hook - result := runtime.InstallHook("bash") - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected InstallHook to return the same runtime instance") - } - - // And original error should be preserved - if runtime.err.Error() != "existing error" { - t.Errorf("Expected original error to be preserved, got %v", runtime.err) - } - }) -} - -func TestRuntime_SetContext(t *testing.T) { - t.Run("SetsContextSuccessfully", func(t *testing.T) { - // Given a runtime with loaded config handler - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // When setting context - result := runtime.SetContext("test-context") - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected SetContext to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And SetContext should have been called on the config handler - // (We can't easily track this without modifying the mock, so we just verify no error occurred) - }) - - t.Run("ReturnsErrorWhenConfigHandlerNotLoaded", func(t *testing.T) { - // Given a runtime without loaded config handler (no pre-loaded dependencies) - runtime := NewRuntime() - - // When setting context - result := runtime.SetContext("test-context") - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected SetContext to return the same runtime instance") - } - - // And error should be set - if runtime.err == nil { - t.Error("Expected error when config handler not loaded") - } - - expectedError := "config handler not loaded - call LoadConfig() first" - if runtime.err.Error() != expectedError { - t.Errorf("Expected error %q, got %q", expectedError, runtime.err.Error()) - } - }) - - t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { - // Given a runtime with an existing error (no pre-loaded dependencies) - runtime := NewRuntime() - runtime.err = errors.New("existing error") - - // When setting context - result := runtime.SetContext("test-context") - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected SetContext to return the same runtime instance") - } - - // And original error should be preserved - if runtime.err.Error() != "existing error" { - t.Errorf("Expected original error to be preserved, got %v", runtime.err) - } - }) - - t.Run("PropagatesConfigHandlerError", func(t *testing.T) { - // Given a runtime with a mock shell that returns an error - mockShell := shell.NewMockShell() - mockShell.GetProjectRootFunc = func() (string, error) { - return "", errors.New("project root error") - } - - // Create runtime with only the mock shell, no mock config handler - runtime := NewRuntime() - runtime.Shell = mockShell - runtime.Injector.Register("shell", mockShell) - runtime.LoadConfig() - - // When setting context - result := runtime.SetContext("test-context") - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected SetContext to return the same runtime instance") - } - - // And error should be propagated from config handler - if runtime.err == nil { - t.Error("Expected error to be propagated from config handler") - } else { - expectedError := "failed to load configuration" - if !strings.Contains(runtime.err.Error(), expectedError) { - t.Errorf("Expected error to contain %q, got %q", expectedError, runtime.err.Error()) - } - } - }) -} - -func TestRuntime_PrintContext(t *testing.T) { - t.Run("PrintsContextSuccessfully", func(t *testing.T) { - // Given a runtime with loaded config handler - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { - return "test-context" - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - var output string - outputFunc := func(s string) { - output = s - } - - // When printing context - result := runtime.PrintContext(outputFunc) - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected PrintContext to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And output should be correct - if output != "test-context" { - t.Errorf("Expected output 'test-context', got %q", output) - } - }) - - t.Run("ReturnsErrorWhenConfigHandlerNotLoaded", func(t *testing.T) { - // Given a runtime without loaded config handler (no pre-loaded dependencies) - runtime := NewRuntime() - - var output string - outputFunc := func(s string) { - output = s - } - - // When printing context - result := runtime.PrintContext(outputFunc) - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected PrintContext to return the same runtime instance") - } - - // And error should be set - if runtime.err == nil { - t.Error("Expected error when config handler not loaded") - } - - expectedError := "config handler not loaded - call LoadConfig() first" - if runtime.err.Error() != expectedError { - t.Errorf("Expected error %q, got %q", expectedError, runtime.err.Error()) - } - - // And output should not be set - if output != "" { - t.Errorf("Expected no output, got %q", output) - } - }) - - 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") - - var output string - outputFunc := func(s string) { - output = s - } - - // When printing context - result := runtime.PrintContext(outputFunc) - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected PrintContext to return the same runtime instance") - } - - // And original error should be preserved - if runtime.err.Error() != "existing error" { - t.Errorf("Expected original error to be preserved, got %v", runtime.err) - } - - // And output should not be set - if output != "" { - t.Errorf("Expected no output, got %q", output) - } - }) -} - -func TestRuntime_WriteResetToken(t *testing.T) { - t.Run("WritesResetTokenSuccessfully", func(t *testing.T) { - // Given a runtime with loaded shell - mocks := setupMocks(t) - mocks.Shell.(*shell.MockShell).WriteResetTokenFunc = func() (string, error) { - return "/tmp/reset-token", nil - } - runtime := NewRuntime(mocks).LoadShell() - - // When writing reset token - result := runtime.WriteResetToken() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected WriteResetToken to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And WriteResetToken should have been called on the shell - // (We can't easily track this without modifying the mock, so we just verify no error occurred) - }) - - t.Run("ReturnsErrorWhenShellNotLoaded", func(t *testing.T) { - // Given a runtime without loaded shell (no pre-loaded dependencies) - runtime := NewRuntime() - - // When writing reset token - result := runtime.WriteResetToken() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected WriteResetToken to return the same runtime instance") - } - - // And error should be set - if runtime.err == nil { - t.Error("Expected error when shell not loaded") - } - - expectedError := "shell not loaded - call LoadShell() first" - if runtime.err.Error() != expectedError { - t.Errorf("Expected error %q, got %q", expectedError, runtime.err.Error()) - } - }) - - t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { - // Given a runtime with an existing error (no pre-loaded dependencies) - runtime := NewRuntime() - runtime.err = errors.New("existing error") - - // When writing reset token - result := runtime.WriteResetToken() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected WriteResetToken to return the same runtime instance") - } - - // And original error should be preserved - if runtime.err.Error() != "existing error" { - t.Errorf("Expected original error to be preserved, got %v", runtime.err) - } - }) - - t.Run("PropagatesShellError", func(t *testing.T) { - // Given a runtime with loaded shell that returns an error - mocks := setupMocks(t) - mocks.Shell.(*shell.MockShell).WriteResetTokenFunc = func() (string, error) { - return "", errors.New("shell error") - } - runtime := NewRuntime(mocks).LoadShell() - - // When writing reset token - result := runtime.WriteResetToken() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected WriteResetToken to return the same runtime instance") - } - - // And error should be propagated - if runtime.err == nil { - t.Error("Expected error to be propagated from shell") - } else { - expectedError := "shell error" - if runtime.err.Error() != expectedError { - t.Errorf("Expected error %q, got %q", expectedError, runtime.err.Error()) - } - } - }) -} - -func TestRuntime_LoadBlueprint(t *testing.T) { - t.Run("LoadsBlueprintSuccessfully", func(t *testing.T) { - // Given a runtime with loaded dependencies - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetStringFunc = func(key string, defaultValue ...string) string { - if key == "cluster.driver" { - return "talos" - } - return "mock-string" - } - mocks.ConfigHandler.(*config.MockConfigHandler).GetContextValuesFunc = func() (map[string]any, error) { - return map[string]any{}, nil - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig().LoadKubernetes() - - // Create local template data to avoid OCI download - tmpDir := t.TempDir() - templateDir := filepath.Join(tmpDir, "contexts", "_template") - if err := os.MkdirAll(templateDir, 0755); err != nil { - t.Fatalf("Failed to create template directory: %v", err) - } - - // Mock GetProjectRoot to return our temp directory - mocks.Shell.(*shell.MockShell).GetProjectRootFunc = func() (string, error) { - return tmpDir, nil - } - - // When loading blueprint - result := runtime.LoadBlueprint() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadBlueprint to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And blueprint handler should be created and registered - if runtime.BlueprintHandler == nil { - t.Error("Expected blueprint handler to be created") - } - - // And artifact builder should NOT be created (separate concern) - if runtime.ArtifactBuilder != nil { - t.Error("Expected artifact builder to NOT be created by LoadBlueprint") - } - - // And components should be registered in injector - if runtime.Injector.Resolve("blueprintHandler") == nil { - t.Error("Expected blueprint handler to be registered in injector") - } - - // Artifact builder should NOT be registered (separate concern) - if runtime.Injector.Resolve("artifactBuilder") != nil { - t.Error("Expected artifact builder to NOT be registered by LoadBlueprint") - } - }) - - t.Run("ReturnsErrorWhenConfigHandlerNotLoaded", func(t *testing.T) { - // Given a runtime without loaded config handler - runtime := NewRuntime() - - // When loading blueprint - result := runtime.LoadBlueprint() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadBlueprint to return the same runtime instance") - } - - // And error should be set - if runtime.err == nil { - t.Error("Expected error when config handler not loaded") - } - - expectedError := "config handler not loaded - call LoadConfig() first" - if runtime.err.Error() != expectedError { - t.Errorf("Expected error %q, got %q", expectedError, runtime.err.Error()) - } - - // And components should not be created - if runtime.BlueprintHandler != nil { - t.Error("Expected blueprint handler to not be created") - } - - if runtime.ArtifactBuilder != nil { - t.Error("Expected artifact builder to not be created") - } - }) - - t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { - // Given a runtime with an existing error - runtime := NewRuntime() - runtime.err = errors.New("existing error") - - // When loading blueprint - result := runtime.LoadBlueprint() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected LoadBlueprint to return the same runtime instance") - } - - // And original error should be preserved - if runtime.err.Error() != "existing error" { - t.Errorf("Expected original error to be preserved, got %v", runtime.err) - } - - // And components should not be created - if runtime.BlueprintHandler != nil { - t.Error("Expected blueprint handler to not be created") - } - - if runtime.ArtifactBuilder != nil { - t.Error("Expected artifact builder to not be created") - } - }) -} - -func TestRuntime_Do(t *testing.T) { - t.Run("ReturnsNilWhenNoError", func(t *testing.T) { - // Given a runtime with no error - mocks := setupMocks(t) - runtime := NewRuntime(mocks) - - // When calling Do - err := runtime.Do() - - // Then should return nil - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("ReturnsErrorWhenErrorSet", func(t *testing.T) { - // Given a runtime with an error - mocks := setupMocks(t) - runtime := NewRuntime(mocks) - expectedError := errors.New("test error") - runtime.err = expectedError - - // When calling Do - err := runtime.Do() - - // Then should return the error - if err != expectedError { - t.Errorf("Expected error %v, got %v", expectedError, err) - } - }) -} -func TestRuntime_HandleSessionReset(t *testing.T) { - t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { - // Given a runtime with an existing error - runtime := NewRuntime() - expectedError := errors.New("existing error") - runtime.err = expectedError - - // When handling session reset - result := runtime.HandleSessionReset() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected HandleSessionReset to return the same runtime instance") - } - - // And original error should be preserved - if runtime.err != expectedError { - t.Errorf("Expected original error to be preserved, got %v", runtime.err) - } - }) - - t.Run("ReturnsErrorWhenShellNotLoaded", func(t *testing.T) { - // Given a runtime without loaded shell - runtime := NewRuntime() - - // When handling session reset - result := runtime.HandleSessionReset() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected HandleSessionReset to return the same runtime instance") - } - - // And error should be set - if runtime.err == nil { - t.Error("Expected error when shell not loaded") - } - - expectedError := "shell not loaded - call LoadShell() first" - if runtime.err.Error() != expectedError { - t.Errorf("Expected error %q, got %q", expectedError, runtime.err.Error()) - } - }) - - t.Run("ResetsWhenNoSessionToken", func(t *testing.T) { - // Given a runtime with loaded shell and no session token - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell() - - // Ensure no session token is set - originalToken := os.Getenv("WINDSOR_SESSION_TOKEN") - os.Unsetenv("WINDSOR_SESSION_TOKEN") - defer func() { - if originalToken != "" { - os.Setenv("WINDSOR_SESSION_TOKEN", originalToken) - } - }() - - // Mock CheckResetFlags to return false (no reset flags) - mocks.Shell.(*shell.MockShell).CheckResetFlagsFunc = func() (bool, error) { - return false, nil - } - - // Track if Reset was called - resetCalled := false - mocks.Shell.(*shell.MockShell).ResetFunc = func(...bool) { - resetCalled = true - } - - // When handling session reset - result := runtime.HandleSessionReset() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected HandleSessionReset to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And reset should be called - if !resetCalled { - t.Error("Expected shell reset to be called when no session token") - } - - // And NO_CACHE should be set - if os.Getenv("NO_CACHE") != "true" { - t.Error("Expected NO_CACHE to be set to true") - } - - // Clean up NO_CACHE - os.Unsetenv("NO_CACHE") - }) - - t.Run("ResetsWhenResetFlagsTrue", func(t *testing.T) { - // Given a runtime with loaded shell and session token - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell() - - // Set session token - originalToken := os.Getenv("WINDSOR_SESSION_TOKEN") - os.Setenv("WINDSOR_SESSION_TOKEN", "test-token") - defer func() { - if originalToken != "" { - os.Setenv("WINDSOR_SESSION_TOKEN", originalToken) - } else { - os.Unsetenv("WINDSOR_SESSION_TOKEN") - } - }() - - // Mock CheckResetFlags to return true (reset flags detected) - mocks.Shell.(*shell.MockShell).CheckResetFlagsFunc = func() (bool, error) { - return true, nil - } - - // Track if Reset was called - resetCalled := false - mocks.Shell.(*shell.MockShell).ResetFunc = func(...bool) { - resetCalled = true - } - - // When handling session reset - result := runtime.HandleSessionReset() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected HandleSessionReset to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And reset should be called - if !resetCalled { - t.Error("Expected shell reset to be called when reset flags are true") - } - - // And NO_CACHE should be set - if os.Getenv("NO_CACHE") != "true" { - t.Error("Expected NO_CACHE to be set to true") - } - - // Clean up NO_CACHE - os.Unsetenv("NO_CACHE") - }) - - t.Run("DoesNotResetWhenContextChanged", func(t *testing.T) { - // Given a runtime with loaded shell, config handler, and session token - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // Set session token - originalToken := os.Getenv("WINDSOR_SESSION_TOKEN") - os.Setenv("WINDSOR_SESSION_TOKEN", "test-token") - defer func() { - if originalToken != "" { - os.Setenv("WINDSOR_SESSION_TOKEN", originalToken) - } else { - os.Unsetenv("WINDSOR_SESSION_TOKEN") - } - }() - - // Set WINDSOR_CONTEXT to differ from current context - originalContext := os.Getenv("WINDSOR_CONTEXT") - os.Setenv("WINDSOR_CONTEXT", "different-context") - defer func() { - if originalContext != "" { - os.Setenv("WINDSOR_CONTEXT", originalContext) - } else { - os.Unsetenv("WINDSOR_CONTEXT") - } - }() - - // Mock CheckResetFlags to return false (no reset flags) - mocks.Shell.(*shell.MockShell).CheckResetFlagsFunc = func() (bool, error) { - return false, nil - } - - // Mock GetContext to return a different context - mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { - return "current-context" - } - - // Track if Reset was called - resetCalled := false - mocks.Shell.(*shell.MockShell).ResetFunc = func(...bool) { - resetCalled = true - } - - // When handling session reset - result := runtime.HandleSessionReset() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected HandleSessionReset to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And reset should NOT be called - if resetCalled { - t.Error("Expected shell reset to NOT be called when context changed (logic not present)") - } - - // And NO_CACHE should NOT be set - if os.Getenv("NO_CACHE") == "true" { - t.Error("Expected NO_CACHE to NOT be set when context changed (logic not present)") - } - }) - - t.Run("DoesNotResetWhenNoResetNeeded", func(t *testing.T) { - // Given a runtime with loaded shell, config handler, and session token - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // Set session token - originalToken := os.Getenv("WINDSOR_SESSION_TOKEN") - os.Setenv("WINDSOR_SESSION_TOKEN", "test-token") - defer func() { - if originalToken != "" { - os.Setenv("WINDSOR_SESSION_TOKEN", originalToken) - } else { - os.Unsetenv("WINDSOR_SESSION_TOKEN") - } - }() - - // Set WINDSOR_CONTEXT to match current context (no context change) - originalContext := os.Getenv("WINDSOR_CONTEXT") - os.Setenv("WINDSOR_CONTEXT", "current-context") - defer func() { - if originalContext != "" { - os.Setenv("WINDSOR_CONTEXT", originalContext) - } else { - os.Unsetenv("WINDSOR_CONTEXT") - } - }() - - // Mock CheckResetFlags to return false (no reset flags) - mocks.Shell.(*shell.MockShell).CheckResetFlagsFunc = func() (bool, error) { - return false, nil - } - - // Mock GetContext to return the same context as WINDSOR_CONTEXT - mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { - return "current-context" - } - - // Track if Reset was called - resetCalled := false - mocks.Shell.(*shell.MockShell).ResetFunc = func(...bool) { - resetCalled = true - } - - // When handling session reset - result := runtime.HandleSessionReset() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected HandleSessionReset to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And reset should NOT be called - if resetCalled { - t.Error("Expected shell reset to NOT be called when no reset needed") - } - - // And NO_CACHE should NOT be set - if os.Getenv("NO_CACHE") == "true" { - t.Error("Expected NO_CACHE to NOT be set when no reset needed") - } - }) - - t.Run("PropagatesCheckResetFlagsError", func(t *testing.T) { - // Given a runtime with loaded shell - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell() - - // Set session token - originalToken := os.Getenv("WINDSOR_SESSION_TOKEN") - os.Setenv("WINDSOR_SESSION_TOKEN", "test-token") - defer func() { - if originalToken != "" { - os.Setenv("WINDSOR_SESSION_TOKEN", originalToken) - } else { - os.Unsetenv("WINDSOR_SESSION_TOKEN") - } - }() - - // Mock CheckResetFlags to return an error - expectedError := errors.New("check reset flags error") - mocks.Shell.(*shell.MockShell).CheckResetFlagsFunc = func() (bool, error) { - return false, expectedError - } - - // When handling session reset - result := runtime.HandleSessionReset() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected HandleSessionReset to return the same runtime instance") - } - - // And error should be propagated - if runtime.err == nil { - t.Error("Expected error to be propagated from CheckResetFlags") - } else { - expectedErrorMsg := "failed to check reset flags: check reset flags error" - if runtime.err.Error() != expectedErrorMsg { - t.Errorf("Expected error %q, got %q", expectedErrorMsg, runtime.err.Error()) - } - } - }) - - t.Run("PropagatesSetenvError", func(t *testing.T) { - // Given a runtime with loaded shell and no session token (to trigger reset) - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell() - - // Ensure no session token is set - originalToken := os.Getenv("WINDSOR_SESSION_TOKEN") - os.Unsetenv("WINDSOR_SESSION_TOKEN") - defer func() { - if originalToken != "" { - os.Setenv("WINDSOR_SESSION_TOKEN", originalToken) - } - }() - - // Mock CheckResetFlags to return false (no reset flags) - mocks.Shell.(*shell.MockShell).CheckResetFlagsFunc = func() (bool, error) { - return false, nil - } - - // Mock Reset to succeed - mocks.Shell.(*shell.MockShell).ResetFunc = func(...bool) { - // Reset succeeds - } - - // When handling session reset - result := runtime.HandleSessionReset() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected HandleSessionReset to return the same runtime instance") - } - - // And no error should be set (os.Setenv typically succeeds in tests) - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And NO_CACHE should be set - if os.Getenv("NO_CACHE") != "true" { - t.Error("Expected NO_CACHE to be set to true") - } - - // Clean up NO_CACHE - os.Unsetenv("NO_CACHE") - }) -} - -func TestRuntime_CheckTrustedDirectory(t *testing.T) { - t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { - // Given a runtime with an existing error - runtime := NewRuntime() - expectedError := errors.New("existing error") - runtime.err = expectedError - - // When checking trusted directory - result := runtime.CheckTrustedDirectory() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected CheckTrustedDirectory to return the same runtime instance") - } - - // And original error should be preserved - if runtime.err != expectedError { - t.Errorf("Expected original error to be preserved, got %v", runtime.err) - } - }) - - t.Run("ReturnsErrorWhenShellNotLoaded", func(t *testing.T) { - // Given a runtime without loaded shell - runtime := NewRuntime() - - // When checking trusted directory - result := runtime.CheckTrustedDirectory() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected CheckTrustedDirectory to return the same runtime instance") - } - - // And error should be set - if runtime.err == nil { - t.Error("Expected error when shell not loaded") - } - - expectedError := "shell not loaded - call LoadShell() first" - if runtime.err.Error() != expectedError { - t.Errorf("Expected error %q, got %q", expectedError, runtime.err.Error()) - } - }) - - t.Run("SucceedsWhenDirectoryIsTrusted", func(t *testing.T) { - // Given a runtime with loaded shell - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell() - - // Mock CheckTrustedDirectory to succeed - mocks.Shell.(*shell.MockShell).CheckTrustedDirectoryFunc = func() error { - return nil - } - - // When checking trusted directory - result := runtime.CheckTrustedDirectory() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected CheckTrustedDirectory to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - }) - - t.Run("PropagatesShellError", func(t *testing.T) { - // Given a runtime with loaded shell - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell() - - // Mock CheckTrustedDirectory to return an error - expectedError := errors.New("trusted directory check failed") - mocks.Shell.(*shell.MockShell).CheckTrustedDirectoryFunc = func() error { - return expectedError - } - - // When checking trusted directory - result := runtime.CheckTrustedDirectory() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected CheckTrustedDirectory to return the same runtime instance") - } - - // And error should be set with custom message - if runtime.err == nil { - t.Error("Expected error to be set") - } else { - expectedErrorMsg := "not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve" - if runtime.err.Error() != expectedErrorMsg { - t.Errorf("Expected error %q, got %q", expectedErrorMsg, runtime.err.Error()) - } - } - }) - - t.Run("PropagatesProjectRootError", func(t *testing.T) { - // Given a runtime with loaded shell - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell() - - // Mock CheckTrustedDirectory to return a project root error - expectedError := errors.New("Error getting project root directory: getwd failed") - mocks.Shell.(*shell.MockShell).CheckTrustedDirectoryFunc = func() error { - return expectedError - } - - // When checking trusted directory - result := runtime.CheckTrustedDirectory() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected CheckTrustedDirectory to return the same runtime instance") - } - - // And error should be set with custom message - if runtime.err == nil { - t.Error("Expected error to be set") - } else { - expectedErrorMsg := "not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve" - if runtime.err.Error() != expectedErrorMsg { - t.Errorf("Expected error %q, got %q", expectedErrorMsg, runtime.err.Error()) - } - } - }) - - t.Run("PropagatesTrustedFileNotExistError", func(t *testing.T) { - // Given a runtime with loaded shell - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell() - - // Mock CheckTrustedDirectory to return a trusted file not exist error - expectedError := errors.New("Trusted file does not exist") - mocks.Shell.(*shell.MockShell).CheckTrustedDirectoryFunc = func() error { - return expectedError - } - - // When checking trusted directory - result := runtime.CheckTrustedDirectory() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected CheckTrustedDirectory to return the same runtime instance") - } - - // And error should be set with custom message - if runtime.err == nil { - t.Error("Expected error to be set") - } else { - expectedErrorMsg := "not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve" - if runtime.err.Error() != expectedErrorMsg { - t.Errorf("Expected error %q, got %q", expectedErrorMsg, runtime.err.Error()) - } - } - }) - - t.Run("PropagatesNotTrustedError", func(t *testing.T) { - // Given a runtime with loaded shell - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell() - - // Mock CheckTrustedDirectory to return a not trusted error - expectedError := errors.New("Current directory not in the trusted list") - mocks.Shell.(*shell.MockShell).CheckTrustedDirectoryFunc = func() error { - return expectedError - } - - // When checking trusted directory - result := runtime.CheckTrustedDirectory() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected CheckTrustedDirectory to return the same runtime instance") - } - - // And error should be set with custom message - if runtime.err == nil { - t.Error("Expected error to be set") - } else { - expectedErrorMsg := "not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve" - if runtime.err.Error() != expectedErrorMsg { - t.Errorf("Expected error %q, got %q", expectedErrorMsg, runtime.err.Error()) - } - } - }) -} - -func TestRuntime_PrintEnvVars(t *testing.T) { - t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { - // Given a runtime with an existing error - runtime := NewRuntime() - expectedError := errors.New("existing error") - runtime.err = expectedError - - opts := EnvVarsOptions{ - OutputFunc: func(string) {}, - } - - // When printing environment variables - result := runtime.PrintEnvVars(opts) - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected PrintEnvVars to return the same runtime instance") - } - - // And original error should be preserved - if runtime.err != expectedError { - t.Errorf("Expected original error to be preserved, got %v", runtime.err) - } - }) - - t.Run("PrintsEnvVarsSuccessfully", func(t *testing.T) { - // Given a runtime with loaded shell and pre-populated environment variables - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell() - - // Pre-populate r.EnvVars (simulating what LoadEnvVars would do) - runtime.EnvVars = map[string]string{ - "VAR1": "value1", - "VAR2": "value2", - "VAR3": "value3", - } - - // Track output - var output string - opts := EnvVarsOptions{ - Export: true, - OutputFunc: func(s string) { output = s }, - } - - // Mock shell RenderEnvVars - expectedEnvVars := map[string]string{ - "VAR1": "value1", - "VAR2": "value2", - "VAR3": "value3", - } - mocks.Shell.(*shell.MockShell).RenderEnvVarsFunc = func(envVars map[string]string, export bool) string { - if !reflect.DeepEqual(envVars, expectedEnvVars) { - t.Errorf("Expected env vars %v, got %v", expectedEnvVars, envVars) - } - if !export { - t.Error("Expected export to be true") - } - return "export VAR1=value1\nexport VAR2=value2\nexport VAR3=value3" - } - - // When printing environment variables - result := runtime.PrintEnvVars(opts) - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected PrintEnvVars to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And output should be captured - expectedOutput := "export VAR1=value1\nexport VAR2=value2\nexport VAR3=value3" - if output != expectedOutput { - t.Errorf("Expected output %q, got %q", expectedOutput, output) - } - }) - - t.Run("HandlesEmptyEnvVars", func(t *testing.T) { - // Given a runtime with loaded shell and no environment printers - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell() - - // Track if output function is called - outputCalled := false - opts := EnvVarsOptions{ - OutputFunc: func(string) { outputCalled = true }, - } - - // When printing environment variables - result := runtime.PrintEnvVars(opts) - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected PrintEnvVars to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And output function should not be called - if outputCalled { - t.Error("Expected output function to not be called when no env vars") - } - }) - -} - -func TestRuntime_PrintAliases(t *testing.T) { - t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { - // Given a runtime with an existing error - runtime := NewRuntime() - expectedError := errors.New("existing error") - runtime.err = expectedError - - // When printing aliases - result := runtime.PrintAliases(func(string) {}) - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected PrintAliases to return the same runtime instance") - } - - // And original error should be preserved - if runtime.err != expectedError { - t.Errorf("Expected original error to be preserved, got %v", runtime.err) - } - }) - - t.Run("PrintsAliasesSuccessfully", func(t *testing.T) { - // Given a runtime with loaded shell and environment printers - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell() - - // Set up mock environment printers - mockPrinter1 := envvars.NewMockEnvPrinter() - mockPrinter1.GetAliasFunc = func() (map[string]string, error) { - return map[string]string{"alias1": "command1", "alias2": "command2"}, nil - } - - mockPrinter2 := envvars.NewMockEnvPrinter() - mockPrinter2.GetAliasFunc = func() (map[string]string, error) { - return map[string]string{"alias3": "command3"}, nil - } - - runtime.EnvPrinters.AwsEnv = mockPrinter1 - runtime.EnvPrinters.WindsorEnv = mockPrinter2 - - // Track output - var output string - outputFunc := func(s string) { output = s } - - // Mock shell RenderAliases - expectedAliases := map[string]string{ - "alias1": "command1", - "alias2": "command2", - "alias3": "command3", - } - mocks.Shell.(*shell.MockShell).RenderAliasesFunc = func(aliases map[string]string) string { - if !reflect.DeepEqual(aliases, expectedAliases) { - t.Errorf("Expected aliases %v, got %v", expectedAliases, aliases) - } - return "alias alias1='command1'\nalias alias2='command2'\nalias alias3='command3'" - } - - // When printing aliases - result := runtime.PrintAliases(outputFunc) - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected PrintAliases to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And output should be captured - expectedOutput := "alias alias1='command1'\nalias alias2='command2'\nalias alias3='command3'" - if output != expectedOutput { - t.Errorf("Expected output %q, got %q", expectedOutput, output) - } - }) - - t.Run("HandlesEmptyAliases", func(t *testing.T) { - // Given a runtime with loaded shell and no environment printers - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell() - - // Track if output function is called - outputCalled := false - outputFunc := func(string) { outputCalled = true } - - // When printing aliases - result := runtime.PrintAliases(outputFunc) - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected PrintAliases to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And output function should not be called - if outputCalled { - t.Error("Expected output function to not be called when no aliases") - } - }) - - t.Run("PropagatesAliasError", func(t *testing.T) { - // Given a runtime with loaded shell and environment printer that returns error - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell() - - // Set up mock environment printer that returns error - mockPrinter := envvars.NewMockEnvPrinter() - expectedError := errors.New("alias error") - mockPrinter.GetAliasFunc = func() (map[string]string, error) { - return nil, expectedError - } - - // Set up WindsorEnv printer to avoid panic - windsorPrinter := envvars.NewMockEnvPrinter() - windsorPrinter.GetAliasFunc = func() (map[string]string, error) { - return map[string]string{}, nil - } - - runtime.EnvPrinters.AwsEnv = mockPrinter - runtime.EnvPrinters.WindsorEnv = windsorPrinter - - // When printing aliases - result := runtime.PrintAliases(func(string) {}) - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected PrintAliases to return the same runtime instance") - } - - // And error should be set - if runtime.err == nil { - t.Error("Expected error to be set") - } else { - expectedErrorMsg := "error getting aliases: alias error" - if runtime.err.Error() != expectedErrorMsg { - t.Errorf("Expected error %q, got %q", expectedErrorMsg, runtime.err.Error()) - } - } - }) -} - -func TestRuntime_ExecutePostEnvHook(t *testing.T) { - t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { - // Given a runtime with an existing error - runtime := NewRuntime() - expectedError := errors.New("existing error") - runtime.err = expectedError - - // When executing post env hook - result := runtime.ExecutePostEnvHook(true) - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected ExecutePostEnvHook to return the same runtime instance") - } - - // And original error should be preserved - if runtime.err != expectedError { - t.Errorf("Expected original error to be preserved, got %v", runtime.err) - } - }) - - t.Run("ExecutesPostEnvHooksSuccessfully", func(t *testing.T) { - // Given a runtime with environment printers - mocks := setupMocks(t) - runtime := NewRuntime(mocks) - - // Set up mock environment printers - hook1Called := false - hook2Called := false - - mockPrinter1 := envvars.NewMockEnvPrinter() - mockPrinter1.PostEnvHookFunc = func(directory ...string) error { - hook1Called = true - return nil - } - - mockPrinter2 := envvars.NewMockEnvPrinter() - mockPrinter2.PostEnvHookFunc = func(directory ...string) error { - hook2Called = true - return nil - } - - runtime.EnvPrinters.AwsEnv = mockPrinter1 - runtime.EnvPrinters.WindsorEnv = mockPrinter2 - - // When executing post env hook - result := runtime.ExecutePostEnvHook(true) - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected ExecutePostEnvHook to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And hooks should be called - if !hook1Called { - t.Error("Expected first hook to be called") - } - if !hook2Called { - t.Error("Expected second hook to be called") - } - }) - - t.Run("HandlesHookErrorWithVerboseTrue", func(t *testing.T) { - // Given a runtime with environment printer that returns error - mocks := setupMocks(t) - runtime := NewRuntime(mocks) - - // Set up mock environment printer that returns error - mockPrinter := envvars.NewMockEnvPrinter() - expectedError := errors.New("hook error") - mockPrinter.PostEnvHookFunc = func(directory ...string) error { - return expectedError - } - - // Set up WindsorEnv printer to avoid panic - windsorPrinter := envvars.NewMockEnvPrinter() - windsorPrinter.PostEnvHookFunc = func(directory ...string) error { - return nil - } - - runtime.EnvPrinters.AwsEnv = mockPrinter - runtime.EnvPrinters.WindsorEnv = windsorPrinter - - // When executing post env hook with verbose true - result := runtime.ExecutePostEnvHook(true) - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected ExecutePostEnvHook to return the same runtime instance") - } - - // And error should be set - if runtime.err == nil { - t.Error("Expected error to be set") - } else { - expectedErrorMsg := "failed to execute post env hooks: hook error" - if runtime.err.Error() != expectedErrorMsg { - t.Errorf("Expected error %q, got %q", expectedErrorMsg, runtime.err.Error()) - } - } - }) - - t.Run("HandlesHookErrorWithVerboseFalse", func(t *testing.T) { - // Given a runtime with environment printer that returns error - mocks := setupMocks(t) - runtime := NewRuntime(mocks) - - // Set up mock environment printer that returns error - mockPrinter := envvars.NewMockEnvPrinter() - expectedError := errors.New("hook error") - mockPrinter.PostEnvHookFunc = func(directory ...string) error { - return expectedError - } - - // Set up WindsorEnv printer to avoid panic - windsorPrinter := envvars.NewMockEnvPrinter() - windsorPrinter.PostEnvHookFunc = func(directory ...string) error { - return nil - } - - runtime.EnvPrinters.AwsEnv = mockPrinter - runtime.EnvPrinters.WindsorEnv = windsorPrinter - - // When executing post env hook with verbose false - result := runtime.ExecutePostEnvHook(false) - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected ExecutePostEnvHook to return the same runtime instance") - } - - // And no error should be set (verbose false suppresses errors) - if runtime.err != nil { - t.Errorf("Expected no error when verbose false, got %v", runtime.err) - } - }) - - t.Run("HandlesMultipleHookErrors", func(t *testing.T) { - // Given a runtime with multiple environment printers that return errors - mocks := setupMocks(t) - runtime := NewRuntime(mocks) - - // Set up mock environment printers that return errors - mockPrinter1 := envvars.NewMockEnvPrinter() - error1 := errors.New("hook error 1") - mockPrinter1.PostEnvHookFunc = func(directory ...string) error { - return error1 - } - - mockPrinter2 := envvars.NewMockEnvPrinter() - error2 := errors.New("hook error 2") - mockPrinter2.PostEnvHookFunc = func(directory ...string) error { - return error2 - } - - runtime.EnvPrinters.AwsEnv = mockPrinter1 - runtime.EnvPrinters.WindsorEnv = mockPrinter2 - - // When executing post env hook - result := runtime.ExecutePostEnvHook(true) - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected ExecutePostEnvHook to return the same runtime instance") - } - - // And error should be set with first error - if runtime.err == nil { - t.Error("Expected error to be set") - } else { - expectedErrorMsg := "failed to execute post env hooks: hook error 1" - if runtime.err.Error() != expectedErrorMsg { - t.Errorf("Expected error %q, got %q", expectedErrorMsg, runtime.err.Error()) - } - } - }) - - t.Run("SkipsNilPrinters", func(t *testing.T) { - // Given a runtime with some nil environment printers - mocks := setupMocks(t) - runtime := NewRuntime(mocks) - - // Set up one mock environment printer - hookCalled := false - mockPrinter := envvars.NewMockEnvPrinter() - mockPrinter.PostEnvHookFunc = func(directory ...string) error { - hookCalled = true - return nil - } - - // Set up WindsorEnv printer to avoid panic - windsorPrinter := envvars.NewMockEnvPrinter() - windsorPrinter.PostEnvHookFunc = func(directory ...string) error { - return nil - } - - runtime.EnvPrinters.AwsEnv = mockPrinter - runtime.EnvPrinters.WindsorEnv = windsorPrinter - // Other printers remain nil - - // When executing post env hook - result := runtime.ExecutePostEnvHook(true) - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected ExecutePostEnvHook to return the same runtime instance") - } - - // And no error should be set - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - - // And hook should be called - if !hookCalled { - t.Error("Expected hook to be called") - } - }) -} - -func TestRuntime_getAllEnvPrinters(t *testing.T) { - t.Run("ReturnsAllNonNilPrinters", func(t *testing.T) { - // Given a runtime with some environment printers set - mocks := setupMocks(t) - runtime := NewRuntime(mocks) - - // Set up some mock environment printers - mockPrinter1 := envvars.NewMockEnvPrinter() - mockPrinter2 := envvars.NewMockEnvPrinter() - mockPrinter3 := envvars.NewMockEnvPrinter() - - runtime.EnvPrinters.AwsEnv = mockPrinter1 - runtime.EnvPrinters.AzureEnv = mockPrinter2 - runtime.EnvPrinters.WindsorEnv = mockPrinter3 - // Other printers remain nil - - // When getting all environment printers - printers := runtime.getAllEnvPrinters() - - // Then should return only non-nil printers - if len(printers) != 3 { - t.Errorf("Expected 3 printers, got %d", len(printers)) - } - - // And should include the set printers - foundAws := false - foundAzure := false - foundWindsor := false - - for _, printer := range printers { - if printer == mockPrinter1 { - foundAws = true - } - if printer == mockPrinter2 { - foundAzure = true - } - if printer == mockPrinter3 { - foundWindsor = true - } - } - - if !foundAws { - t.Error("Expected AWS printer to be included") - } - if !foundAzure { - t.Error("Expected Azure printer to be included") - } - if !foundWindsor { - t.Error("Expected Windsor printer to be included") - } - }) - - t.Run("EnsuresWindsorEnvIsLast", func(t *testing.T) { - // Given a runtime with WindsorEnv and other printers - mocks := setupMocks(t) - runtime := NewRuntime(mocks) - - // Set up mock environment printers - mockPrinter1 := envvars.NewMockEnvPrinter() - mockPrinter2 := envvars.NewMockEnvPrinter() - windsorPrinter := envvars.NewMockEnvPrinter() - - runtime.EnvPrinters.AwsEnv = mockPrinter1 - runtime.EnvPrinters.AzureEnv = mockPrinter2 - runtime.EnvPrinters.WindsorEnv = windsorPrinter - - // When getting all environment printers - printers := runtime.getAllEnvPrinters() - - // Then WindsorEnv should be last - if len(printers) == 0 { - t.Error("Expected at least one printer") - } else if printers[len(printers)-1] != windsorPrinter { - t.Error("Expected WindsorEnv to be the last printer") - } - }) - - t.Run("ReturnsEmptySliceWhenNoPrinters", func(t *testing.T) { - // Given a runtime with no environment printers set - mocks := setupMocks(t) - runtime := NewRuntime(mocks) - - // When getting all environment printers - printers := runtime.getAllEnvPrinters() - - // Then should return empty slice - if len(printers) != 0 { - t.Errorf("Expected 0 printers, got %d", len(printers)) - } - }) -} - -func TestRuntime_WorkstationUp(t *testing.T) { - t.Run("StartsWorkstationSuccessfully", func(t *testing.T) { - // Given a runtime with loaded dependencies - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { - return "test-context" - } - mocks.Shell.(*shell.MockShell).GetProjectRootFunc = func() (string, error) { - return "/test/project", nil - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // When starting workstation - result := runtime.WorkstationUp() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected WorkstationUp to return the same runtime instance") - } - - // And error should be set due to workstation creation failure - // (This is expected since we don't have a real workstation setup) - if runtime.err == nil { - t.Error("Expected error to be set due to workstation creation failure") - } - }) - - t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { - // Given a runtime with an existing error - mocks := setupMocks(t) - runtime := NewRuntime(mocks) - expectedError := errors.New("existing error") - runtime.err = expectedError - - // When starting workstation - result := runtime.WorkstationUp() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected WorkstationUp to return the same runtime instance") - } - - // And error should remain unchanged - if runtime.err != expectedError { - t.Errorf("Expected error %v, got %v", expectedError, runtime.err) - } - }) - - t.Run("ReturnsErrorWhenConfigHandlerNotLoaded", func(t *testing.T) { - // Given a runtime without loaded config handler - runtime := NewRuntime() - - // When starting workstation - result := runtime.WorkstationUp() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected WorkstationUp to return the same runtime instance") - } - - // And error should be set - if runtime.err == nil { - t.Error("Expected error to be set") - } else { - expectedError := "config handler not loaded" - if !strings.Contains(runtime.err.Error(), expectedError) { - t.Errorf("Expected error to contain %q, got %q", expectedError, runtime.err.Error()) - } - } - }) - - t.Run("ReturnsErrorWhenShellNotLoaded", func(t *testing.T) { - // Given a runtime with config handler but no shell - mocks := setupMocks(t) - runtime := NewRuntime(mocks) - // Manually set config handler without calling LoadConfig (which loads shell) - runtime.ConfigHandler = mocks.ConfigHandler - // Explicitly set shell to nil - runtime.Shell = nil - - // When starting workstation - result := runtime.WorkstationUp() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected WorkstationUp to return the same runtime instance") - } - - // And error should be set - if runtime.err == nil { - t.Error("Expected error to be set") - } else { - expectedError := "shell not loaded" - if !strings.Contains(runtime.err.Error(), expectedError) { - t.Errorf("Expected error to contain %q, got %q", expectedError, runtime.err.Error()) - } - } - }) - - t.Run("ReturnsErrorWhenInjectorNotAvailable", func(t *testing.T) { - // Given a runtime with loaded dependencies but no injector - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - runtime.Injector = nil - - // When starting workstation - result := runtime.WorkstationUp() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected WorkstationUp to return the same runtime instance") - } - - // And error should be set - if runtime.err == nil { - t.Error("Expected error to be set") - } else { - expectedError := "injector not available" - if !strings.Contains(runtime.err.Error(), expectedError) { - t.Errorf("Expected error to contain %q, got %q", expectedError, runtime.err.Error()) - } - } - }) - - t.Run("ReturnsErrorWhenNoContextSet", func(t *testing.T) { - // Given a runtime with loaded dependencies but no context - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { - return "" - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // When starting workstation - result := runtime.WorkstationUp() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected WorkstationUp to return the same runtime instance") - } - - // And error should be set - if runtime.err == nil { - t.Error("Expected error to be set") - } else { - expectedError := "no context set" - if !strings.Contains(runtime.err.Error(), expectedError) { - t.Errorf("Expected error to contain %q, got %q", expectedError, runtime.err.Error()) - } - } - }) - - t.Run("PropagatesProjectRootError", func(t *testing.T) { - // Given a runtime with loaded dependencies - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { - return "test-context" - } - expectedError := errors.New("project root error") - mocks.Shell.(*shell.MockShell).GetProjectRootFunc = func() (string, error) { - return "", expectedError - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // When starting workstation - result := runtime.WorkstationUp() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected WorkstationUp to return the same runtime instance") - } - - // And error should be propagated - if runtime.err == nil { - t.Error("Expected error to be propagated") - } else { - expectedErrorText := "failed to get project root" - if !strings.Contains(runtime.err.Error(), expectedErrorText) { - t.Errorf("Expected error to contain %q, got %q", expectedErrorText, runtime.err.Error()) - } - } - }) -} - -func TestRuntime_WorkstationDown(t *testing.T) { - t.Run("StopsWorkstationSuccessfully", func(t *testing.T) { - // Given a runtime with loaded dependencies - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { - return "test-context" - } - mocks.ConfigHandler.(*config.MockConfigHandler).GetStringFunc = func(key string, defaultValue ...string) string { - if key == "network.cidr_block" { - return "10.5.0.0/16" - } - if key == "vm.driver" { - return "docker-desktop" - } - if len(defaultValue) > 0 { - return defaultValue[0] - } - return "mock-string" - } - mocks.Shell.(*shell.MockShell).GetProjectRootFunc = func() (string, error) { - return "/test/project", nil - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // When stopping workstation - result := runtime.WorkstationDown() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected WorkstationDown to return the same runtime instance") - } - - // And no error should be set (workstation down succeeds even with minimal setup) - if runtime.err != nil { - t.Errorf("Expected no error, got %v", runtime.err) - } - }) - - t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { - // Given a runtime with an existing error - mocks := setupMocks(t) - runtime := NewRuntime(mocks) - expectedError := errors.New("existing error") - runtime.err = expectedError - - // When stopping workstation - result := runtime.WorkstationDown() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected WorkstationDown to return the same runtime instance") - } - - // And error should remain unchanged - if runtime.err != expectedError { - t.Errorf("Expected error %v, got %v", expectedError, runtime.err) - } - }) - - t.Run("ReturnsErrorWhenConfigHandlerNotLoaded", func(t *testing.T) { - // Given a runtime without loaded config handler - runtime := NewRuntime() - - // When stopping workstation - result := runtime.WorkstationDown() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected WorkstationDown to return the same runtime instance") - } - - // And error should be set - if runtime.err == nil { - t.Error("Expected error to be set") - } else { - expectedError := "config handler not loaded" - if !strings.Contains(runtime.err.Error(), expectedError) { - t.Errorf("Expected error to contain %q, got %q", expectedError, runtime.err.Error()) - } - } - }) - - t.Run("ReturnsErrorWhenShellNotLoaded", func(t *testing.T) { - // Given a runtime with config handler but no shell - mocks := setupMocks(t) - runtime := NewRuntime(mocks) - // Manually set config handler without calling LoadConfig (which loads shell) - runtime.ConfigHandler = mocks.ConfigHandler - // Explicitly set shell to nil - runtime.Shell = nil - - // When stopping workstation - result := runtime.WorkstationDown() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected WorkstationDown to return the same runtime instance") - } - - // And error should be set - if runtime.err == nil { - t.Error("Expected error to be set") - } else { - expectedError := "shell not loaded" - if !strings.Contains(runtime.err.Error(), expectedError) { - t.Errorf("Expected error to contain %q, got %q", expectedError, runtime.err.Error()) - } - } - }) - - t.Run("ReturnsErrorWhenInjectorNotAvailable", func(t *testing.T) { - // Given a runtime with loaded dependencies but no injector - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - runtime.Injector = nil - - // When stopping workstation - result := runtime.WorkstationDown() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected WorkstationDown to return the same runtime instance") - } - - // And error should be set - if runtime.err == nil { - t.Error("Expected error to be set") - } else { - expectedError := "injector not available" - if !strings.Contains(runtime.err.Error(), expectedError) { - t.Errorf("Expected error to contain %q, got %q", expectedError, runtime.err.Error()) - } - } - }) - - t.Run("ReturnsErrorWhenNoContextSet", func(t *testing.T) { - // Given a runtime with loaded dependencies but no context - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { - return "" - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // When stopping workstation - result := runtime.WorkstationDown() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected WorkstationDown to return the same runtime instance") - } - - // And error should be set - if runtime.err == nil { - t.Error("Expected error to be set") - } else { - expectedError := "no context set" - if !strings.Contains(runtime.err.Error(), expectedError) { - t.Errorf("Expected error to contain %q, got %q", expectedError, runtime.err.Error()) - } - } - }) - - t.Run("PropagatesProjectRootError", func(t *testing.T) { - // Given a runtime with loaded dependencies - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { - return "test-context" - } - expectedError := errors.New("project root error") - mocks.Shell.(*shell.MockShell).GetProjectRootFunc = func() (string, error) { - return "", expectedError - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // When stopping workstation - result := runtime.WorkstationDown() - - // Then should return the same runtime instance - if result != runtime { - t.Error("Expected WorkstationDown to return the same runtime instance") - } - - // And error should be propagated - if runtime.err == nil { - t.Error("Expected error to be propagated") - } else { - expectedErrorText := "failed to get project root" - if !strings.Contains(runtime.err.Error(), expectedErrorText) { - t.Errorf("Expected error to contain %q, got %q", expectedErrorText, runtime.err.Error()) - } - } - }) -} - -func TestRuntime_createWorkstation(t *testing.T) { - t.Run("CreatesWorkstationSuccessfully", func(t *testing.T) { - // Given a runtime with loaded dependencies - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { - return "test-context" - } - mocks.ConfigHandler.(*config.MockConfigHandler).GetStringFunc = func(key string, defaultValue ...string) string { - if key == "network.cidr_block" { - return "10.5.0.0/16" - } - if key == "vm.driver" { - return "docker-desktop" - } - if len(defaultValue) > 0 { - return defaultValue[0] - } - return "mock-string" - } - mocks.Shell.(*shell.MockShell).GetProjectRootFunc = func() (string, error) { - return "/test/project", nil - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // When creating workstation - ws, err := runtime.createWorkstation() - - // Then should return workstation and no error - if ws == nil { - t.Error("Expected workstation to be created") - } - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("ReturnsErrorWhenConfigHandlerNotLoaded", func(t *testing.T) { - // Given a runtime without loaded config handler - runtime := NewRuntime() - - // When creating workstation - ws, err := runtime.createWorkstation() - - // Then should return error - if ws != nil { - t.Error("Expected workstation to be nil") - } - if err == nil { - t.Error("Expected error to be returned") - } else { - expectedError := "config handler not loaded" - if !strings.Contains(err.Error(), expectedError) { - t.Errorf("Expected error to contain %q, got %q", expectedError, err.Error()) - } - } - }) - - t.Run("ReturnsErrorWhenShellNotLoaded", func(t *testing.T) { - // Given a runtime with config handler but no shell - mocks := setupMocks(t) - runtime := NewRuntime(mocks) - // Manually set config handler without calling LoadConfig (which loads shell) - runtime.ConfigHandler = mocks.ConfigHandler - // Explicitly set shell to nil - runtime.Shell = nil - - // When creating workstation - ws, err := runtime.createWorkstation() - - // Then should return error - if ws != nil { - t.Error("Expected workstation to be nil") - } - if err == nil { - t.Error("Expected error to be returned") - } else { - expectedError := "shell not loaded" - if !strings.Contains(err.Error(), expectedError) { - t.Errorf("Expected error to contain %q, got %q", expectedError, err.Error()) - } - } - }) - - t.Run("ReturnsErrorWhenInjectorNotAvailable", func(t *testing.T) { - // Given a runtime with loaded dependencies but no injector - mocks := setupMocks(t) - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - runtime.Injector = nil - - // When creating workstation - ws, err := runtime.createWorkstation() - - // Then should return error - if ws != nil { - t.Error("Expected workstation to be nil") - } - if err == nil { - t.Error("Expected error to be returned") - } else { - expectedError := "injector not available" - if !strings.Contains(err.Error(), expectedError) { - t.Errorf("Expected error to contain %q, got %q", expectedError, err.Error()) - } - } - }) - - t.Run("ReturnsErrorWhenNoContextSet", func(t *testing.T) { - // Given a runtime with loaded dependencies but no context - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { - return "" - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // When creating workstation - ws, err := runtime.createWorkstation() - - // Then should return error - if ws != nil { - t.Error("Expected workstation to be nil") - } - if err == nil { - t.Error("Expected error to be returned") - } else { - expectedError := "no context set" - if !strings.Contains(err.Error(), expectedError) { - t.Errorf("Expected error to contain %q, got %q", expectedError, err.Error()) - } - } - }) - - t.Run("PropagatesProjectRootError", func(t *testing.T) { - // Given a runtime with loaded dependencies - mocks := setupMocks(t) - mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { - return "test-context" - } - expectedError := errors.New("project root error") - mocks.Shell.(*shell.MockShell).GetProjectRootFunc = func() (string, error) { - return "", expectedError - } - runtime := NewRuntime(mocks).LoadShell().LoadConfig() - - // When creating workstation - ws, err := runtime.createWorkstation() - - // Then should return error - if ws != nil { - t.Error("Expected workstation to be nil") - } - if err == nil { - t.Error("Expected error to be returned") - } else { - expectedErrorText := "failed to get project root" - if !strings.Contains(err.Error(), expectedErrorText) { - t.Errorf("Expected error to contain %q, got %q", expectedErrorText, err.Error()) - } - } - }) -} diff --git a/pkg/runtime/shims.go b/pkg/runtime/shims.go deleted file mode 100644 index 6e64c2890..000000000 --- a/pkg/runtime/shims.go +++ /dev/null @@ -1,26 +0,0 @@ -package runtime - -import "os" - -// Shims provides a testable interface for system operations used by pipelines. -// This struct-based approach allows for better isolation during testing by enabling -// dependency injection of mock implementations for file system and environment operations. -// Each pipeline can use its own Shims instance with customized behavior for testing scenarios. -type Shims struct { - Stat func(name string) (os.FileInfo, error) - Getenv func(key string) string - Setenv func(key, value string) error - RemoveAll func(path string) error -} - -// NewShims creates a new Shims instance with default system call implementations. -// The returned instance provides direct access to os package functions and can be -// used in production environments or as a base for creating test-specific variants. -func NewShims() *Shims { - return &Shims{ - Stat: os.Stat, - Getenv: os.Getenv, - Setenv: os.Setenv, - RemoveAll: os.RemoveAll, - } -}