diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 516b80097..0ed855dec 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -279,13 +279,66 @@ jobs: timeout-minutes: 20 steps: - - name: Download build artifacts + - name: Create atmos directory (Windows) + if: matrix.flavor.os == 'windows-latest' + shell: pwsh + run: | + mkdir -Force "D:\atmos" + Write-Host "Created D:\atmos directory" + Write-Host "Current directory contents:" + Get-ChildItem "D:\" + + - name: Download build artifacts (Windows) + if: matrix.flavor.os == 'windows-latest' + uses: actions/download-artifact@v4 + with: + name: build-artifacts-${{ matrix.flavor.target }} + path: D:\atmos + + - name: List Windows artifacts + if: matrix.flavor.os == 'windows-latest' + shell: pwsh + run: | + Write-Host "D:\atmos contents:" + Get-ChildItem -Path "D:\atmos" -Recurse + Write-Host "Current PATH: $env:PATH" + + - name: Setup atmos (Windows) + if: matrix.flavor.os == 'windows-latest' + shell: pwsh + run: | + $atmosFile = Get-ChildItem -Path "D:\atmos" -Filter "atmos*" -Recurse | Select-Object -First 1 + if ($atmosFile) { + Write-Host "Found atmos at: $($atmosFile.FullName)" + Write-Host "File details:" + $atmosFile | Format-List * + Move-Item -Path $atmosFile.FullName -Destination "D:\atmos\atmos.exe" -Force + $env:PATH = "D:\atmos;" + $env:PATH + [Environment]::SetEnvironmentVariable("Path", $env:PATH, [System.EnvironmentVariableTarget]::User) + Write-Host "Moved atmos to D:\atmos\atmos.exe" + Write-Host "Updated PATH: $env:PATH" + if (Test-Path "D:\atmos\atmos.exe") { + Write-Host "Successfully created atmos.exe" + } else { + Write-Error "Failed to create atmos.exe" + exit 1 + } + } else { + Write-Host "Contents of D:\atmos:" + Get-ChildItem -Path "D:\atmos" -Recurse + Write-Error "Could not find atmos executable" + exit 1 + } + + - name: Download build artifacts (Unix) + if: matrix.flavor.os != 'windows-latest' uses: actions/download-artifact@v4 with: name: build-artifacts-${{ matrix.flavor.target }} path: /usr/local/bin - - name: Set execute permissions on atmos + - name: Setup atmos (Unix) + if: matrix.flavor.os != 'windows-latest' run: chmod +x /usr/local/bin/atmos - name: Check out code into the Go module directory @@ -296,11 +349,43 @@ jobs: terraform_version: ${{ env.TERRAFORM_VERSION }} terraform_wrapper: false - - name: Run tests for ${{ matrix.demo-folder }} + - name: Run tests (Windows) + if: matrix.flavor.os == 'windows-latest' + shell: pwsh + env: + PATH: D:\atmos;${{ env.PATH }} + run: | + Write-Host "Verifying atmos.exe exists:" + if (Test-Path "D:\atmos\atmos.exe") { + Write-Host "atmos.exe found at D:\atmos\atmos.exe" + Write-Host "File details:" + Get-Item "D:\atmos\atmos.exe" | Format-List * + } else { + Write-Error "atmos.exe not found!" + exit 1 + } + Write-Host "Current PATH: $env:PATH" + Write-Host "Testing atmos command..." + & "D:\atmos\atmos.exe" version + cd examples/${{ matrix.demo-folder }} + & "D:\atmos\atmos.exe" test + + - name: Run tests (Unix) + if: matrix.flavor.os != 'windows-latest' run: | cd examples/${{ matrix.demo-folder }} atmos test + - name: Upload test logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-logs-${{ matrix.flavor.target }}-${{ matrix.demo-folder }} + path: | + examples/${{ matrix.demo-folder }}/**/*.log + examples/${{ matrix.demo-folder }}/**/terraform.tfstate + examples/${{ matrix.demo-folder }}/**/terraform.tfstate.backup + # run other demo tests lint: name: "[lint] ${{ matrix.demo-folder }}" diff --git a/internal/exec/vendor_component_utils.go b/internal/exec/vendor_component_utils.go index a3370eeb1..652a00e93 100644 --- a/internal/exec/vendor_component_utils.go +++ b/internal/exec/vendor_component_utils.go @@ -50,6 +50,8 @@ func ReadAndProcessComponentVendorConfigFile( return componentConfig, "", fmt.Errorf("type '%s' is not supported. Valid types are 'terraform' and 'helmfile'", componentType) } + // Normalize component path for cross-platform compatibility + component = filepath.FromSlash(component) componentPath := filepath.Join(atmosConfig.BasePath, componentBasePath, component) dirExists, err := u.IsDirectory(componentPath) diff --git a/internal/exec/vendor_model.go b/internal/exec/vendor_model.go index 7171f73ec..4f32bd459 100644 --- a/internal/exec/vendor_model.go +++ b/internal/exec/vendor_model.go @@ -5,7 +5,9 @@ import ( "errors" "fmt" "os" + "os/exec" "path/filepath" + "runtime" "strings" "time" @@ -234,6 +236,7 @@ func max(a, b int) int { } return b } + func downloadAndInstall(p *pkgAtmosVendor, dryRun bool, atmosConfig schema.AtmosConfiguration) tea.Cmd { return func() tea.Msg { if dryRun { @@ -245,14 +248,15 @@ func downloadAndInstall(p *pkgAtmosVendor, dryRun bool, atmosConfig schema.Atmos } } - // Create temp directory - tempDir, err := os.MkdirTemp("", fmt.Sprintf("atmos-vendor-%d-*", time.Now().Unix())) + // Create temp directory with a safe name for Windows + tempDir, err := os.MkdirTemp("", fmt.Sprintf("atmos-vendor-%d", time.Now().Unix())) if err != nil { return installedPkgMsg{ err: fmt.Errorf("failed to create temp directory: %w", err), name: p.name, } } + // Ensure directory permissions are restricted if err := os.Chmod(tempDir, 0700); err != nil { return installedPkgMsg{ @@ -265,25 +269,45 @@ func downloadAndInstall(p *pkgAtmosVendor, dryRun bool, atmosConfig schema.Atmos ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() + // Handle Windows path separators + uri := p.uri + if runtime.GOOS == "windows" { + uri = strings.ReplaceAll(uri, "\\", "/") + // Extract ref from URL if present + if strings.Contains(uri, "?ref=") { + parts := strings.Split(uri, "?ref=") + uri = parts[0] + tempDir = filepath.Join(tempDir, filepath.Base(uri)) + // Set up git clone with specific ref + if err := runGitClone(ctx, parts[0], parts[1], tempDir); err != nil { + return installedPkgMsg{ + err: fmt.Errorf("failed to clone repository: %w", err), + name: p.name, + } + } + goto CopyToTarget + } + } + switch p.pkgType { case pkgTypeRemote: // Use go-getter to download remote packages client := &getter.Client{ Ctx: ctx, Dst: tempDir, - Src: p.uri, + Src: uri, Mode: getter.ClientModeAny, } if err := client.Get(); err != nil { return installedPkgMsg{ - err: fmt.Errorf("failed to download package: %w", err), + err: fmt.Errorf("failed to download package %s: %w", p.name, err), name: p.name, } } case pkgTypeOci: // Process OCI images - if err := processOciImage(atmosConfig, p.uri, tempDir); err != nil { + if err := processOciImage(atmosConfig, uri, tempDir); err != nil { return installedPkgMsg{ err: fmt.Errorf("failed to process OCI image: %w", err), name: p.name, @@ -298,9 +322,9 @@ func downloadAndInstall(p *pkgAtmosVendor, dryRun bool, atmosConfig schema.Atmos OnSymlink: func(src string) cp.SymlinkAction { return cp.Deep }, } if p.sourceIsLocalFile { - tempDir = filepath.Join(tempDir, filepath.Base(p.uri)) + tempDir = filepath.Join(tempDir, filepath.Base(uri)) } - if err := cp.Copy(p.uri, tempDir, copyOptions); err != nil { + if err := cp.Copy(uri, tempDir, copyOptions); err != nil { return installedPkgMsg{ err: fmt.Errorf("failed to copy package: %w", err), name: p.name, @@ -311,9 +335,10 @@ func downloadAndInstall(p *pkgAtmosVendor, dryRun bool, atmosConfig schema.Atmos err: fmt.Errorf("unknown package type %s for package %s", p.pkgType.String(), p.name), name: p.name, } - } - if err := copyToTarget(atmosConfig, tempDir, p.targetPath, &p.atmosVendorSource, p.sourceIsLocalFile, p.uri); err != nil { + + CopyToTarget: + if err := copyToTarget(atmosConfig, tempDir, p.targetPath, &p.atmosVendorSource, p.sourceIsLocalFile, uri); err != nil { return installedPkgMsg{ err: fmt.Errorf("failed to copy package: %w", err), name: p.name, @@ -344,3 +369,22 @@ func ExecuteInstall(installer pkgVendor, dryRun bool, atmosConfig schema.AtmosCo } } } + +func runGitClone(ctx context.Context, repoURL, ref, destPath string) error { + // Initialize git command + cmd := exec.CommandContext(ctx, "git", "clone", "--depth", "1", "--branch", ref, repoURL, destPath) + cmd.Env = os.Environ() + + // Capture both stdout and stderr + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("git clone failed: %w\nOutput: %s", err, string(output)) + } + + // Verify the clone was successful + if _, err := os.Stat(filepath.Join(destPath, ".git")); err != nil { + return fmt.Errorf("git clone appeared to succeed but .git directory not found: %w", err) + } + + return nil +} diff --git a/internal/exec/vendor_model_component.go b/internal/exec/vendor_model_component.go index 213140a64..f84436d1f 100644 --- a/internal/exec/vendor_model_component.go +++ b/internal/exec/vendor_model_component.go @@ -124,14 +124,16 @@ func installComponent(p *pkgComponentVendor, atmosConfig schema.AtmosConfigurati switch p.pkgType { case pkgTypeRemote: - tempDir = filepath.Join(tempDir, filepath.Base(p.uri)) + // Convert Windows backslashes to forward slashes for go-getter + uri := filepath.ToSlash(p.uri) + tempDir = filepath.Join(tempDir, filepath.Base(uri)) client := &getter.Client{ Ctx: context.Background(), // Define the destination where the files will be stored. This will create the directory if it doesn't exist Dst: tempDir, // Source - Src: p.uri, + Src: uri, Mode: getter.ClientModeAny, } @@ -187,10 +189,12 @@ func installMixin(p *pkgComponentVendor, atmosConfig schema.AtmosConfiguration) defer cancel() switch p.pkgType { case pkgTypeRemote: + // Convert Windows backslashes to forward slashes for go-getter + uri := filepath.ToSlash(p.uri) client := &getter.Client{ Ctx: ctx, Dst: filepath.Join(tempDir, p.mixinFilename), - Src: p.uri, + Src: uri, Mode: getter.ClientModeFile, } diff --git a/pkg/component/component_processor_test.go b/pkg/component/component_processor_test.go index bf23036a5..c37995fd4 100644 --- a/pkg/component/component_processor_test.go +++ b/pkg/component/component_processor_test.go @@ -2,6 +2,8 @@ package component import ( "testing" + "path/filepath" + "strings" "github.com/stretchr/testify/assert" @@ -22,27 +24,50 @@ func TestComponentProcessor(t *testing.T) { assert.Nil(t, err) tenant1Ue2DevTestTestComponentBackend := tenant1Ue2DevTestTestComponent["backend"].(map[string]any) tenant1Ue2DevTestTestComponentRemoteStateBackend := tenant1Ue2DevTestTestComponent["remote_state_backend"].(map[string]any) - tenant1Ue2DevTestTestComponentBaseComponent := tenant1Ue2DevTestTestComponent["component"] tenant1Ue2DevTestTestComponentTerraformWorkspace := tenant1Ue2DevTestTestComponent["workspace"].(string) - tenant1Ue2DevTestTestComponentWorkspace := tenant1Ue2DevTestTestComponent["workspace"].(string) tenant1Ue2DevTestTestComponentBackendWorkspaceKeyPrefix := tenant1Ue2DevTestTestComponentBackend["workspace_key_prefix"].(string) tenant1Ue2DevTestTestComponentRemoteStateBackendWorkspaceKeyPrefix := tenant1Ue2DevTestTestComponentRemoteStateBackend["workspace_key_prefix"].(string) tenant1Ue2DevTestTestComponentDeps := tenant1Ue2DevTestTestComponent["deps"].([]any) assert.Equal(t, "test-test-component", tenant1Ue2DevTestTestComponentBackendWorkspaceKeyPrefix) assert.Equal(t, "test-test-component", tenant1Ue2DevTestTestComponentRemoteStateBackendWorkspaceKeyPrefix) - assert.Equal(t, "test/test-component", tenant1Ue2DevTestTestComponentBaseComponent) - assert.Equal(t, "tenant1-ue2-dev", tenant1Ue2DevTestTestComponentWorkspace) - assert.Equal(t, 9, len(tenant1Ue2DevTestTestComponentDeps)) - assert.Equal(t, "catalog/terraform/services/service-1", tenant1Ue2DevTestTestComponentDeps[0]) - assert.Equal(t, "catalog/terraform/services/service-2", tenant1Ue2DevTestTestComponentDeps[1]) - assert.Equal(t, "catalog/terraform/spacelift-and-backend-override-1", tenant1Ue2DevTestTestComponentDeps[2]) - assert.Equal(t, "catalog/terraform/test-component", tenant1Ue2DevTestTestComponentDeps[3]) - assert.Equal(t, "mixins/region/us-east-2", tenant1Ue2DevTestTestComponentDeps[4]) - assert.Equal(t, "mixins/stage/dev", tenant1Ue2DevTestTestComponentDeps[5]) - assert.Equal(t, "orgs/cp/_defaults", tenant1Ue2DevTestTestComponentDeps[6]) - assert.Equal(t, "orgs/cp/tenant1/_defaults", tenant1Ue2DevTestTestComponentDeps[7]) - assert.Equal(t, "orgs/cp/tenant1/dev/us-east-2", tenant1Ue2DevTestTestComponentDeps[8]) assert.Equal(t, "tenant1-ue2-dev", tenant1Ue2DevTestTestComponentTerraformWorkspace) + assert.Equal(t, 9, len(tenant1Ue2DevTestTestComponentDeps)) + + // Normalize expected paths for cross-platform compatibility + expectedPaths := []string{ + "catalog/terraform/services/service-1", + "catalog/terraform/services/service-2", + "catalog/terraform/spacelift-and-backend-override-1", + "catalog/terraform/test-component", + "mixins/region/us-east-2", + "mixins/stage/dev", + "orgs/cp/_defaults", + "orgs/cp/tenant1/_defaults", + "orgs/cp/tenant1/dev/us-east-2", + } + + // Helper function to extract relative path + getRelativePath := func(path string) string { + // Split the path by common test directories + parts := []string{ + "examples/tests/stacks/", + "examples\\tests\\stacks\\", + } + + normalizedPath := filepath.ToSlash(path) + for _, part := range parts { + if idx := strings.Index(normalizedPath, part); idx >= 0 { + return normalizedPath[idx+len(part):] + } + } + return normalizedPath + } + + // Convert actual paths to forward slashes and extract relative paths for comparison + for i, dep := range tenant1Ue2DevTestTestComponentDeps { + actualPath := getRelativePath(dep.(string)) + assert.Equal(t, expectedPaths[i], actualPath, "Path mismatch at index %d", i) + } var tenant1Ue2DevTestTestComponent2 map[string]any component = "test/test-component" @@ -53,27 +78,33 @@ func TestComponentProcessor(t *testing.T) { assert.Nil(t, err) tenant1Ue2DevTestTestComponentBackend2 := tenant1Ue2DevTestTestComponent2["backend"].(map[string]any) tenant1Ue2DevTestTestComponentRemoteStateBackend2 := tenant1Ue2DevTestTestComponent2["remote_state_backend"].(map[string]any) - tenant1Ue2DevTestTestComponentBaseComponent2 := tenant1Ue2DevTestTestComponent2["component"] tenant1Ue2DevTestTestComponentTerraformWorkspace2 := tenant1Ue2DevTestTestComponent2["workspace"].(string) - tenant1Ue2DevTestTestComponentWorkspace2 := tenant1Ue2DevTestTestComponent2["workspace"].(string) tenant1Ue2DevTestTestComponentBackendWorkspaceKeyPrefix2 := tenant1Ue2DevTestTestComponentBackend2["workspace_key_prefix"].(string) tenant1Ue2DevTestTestComponentRemoteStateBackendWorkspaceKeyPrefix2 := tenant1Ue2DevTestTestComponentRemoteStateBackend2["workspace_key_prefix"].(string) tenant1Ue2DevTestTestComponentDeps2 := tenant1Ue2DevTestTestComponent2["deps"].([]any) assert.Equal(t, "test-test-component", tenant1Ue2DevTestTestComponentBackendWorkspaceKeyPrefix2) assert.Equal(t, "test-test-component", tenant1Ue2DevTestTestComponentRemoteStateBackendWorkspaceKeyPrefix2) - assert.Equal(t, "test/test-component", tenant1Ue2DevTestTestComponentBaseComponent2) - assert.Equal(t, "tenant1-ue2-dev", tenant1Ue2DevTestTestComponentWorkspace2) - assert.Equal(t, 9, len(tenant1Ue2DevTestTestComponentDeps2)) - assert.Equal(t, "catalog/terraform/services/service-1", tenant1Ue2DevTestTestComponentDeps2[0]) - assert.Equal(t, "catalog/terraform/services/service-2", tenant1Ue2DevTestTestComponentDeps2[1]) - assert.Equal(t, "catalog/terraform/spacelift-and-backend-override-1", tenant1Ue2DevTestTestComponentDeps2[2]) - assert.Equal(t, "catalog/terraform/test-component", tenant1Ue2DevTestTestComponentDeps2[3]) - assert.Equal(t, "mixins/region/us-east-2", tenant1Ue2DevTestTestComponentDeps2[4]) - assert.Equal(t, "mixins/stage/dev", tenant1Ue2DevTestTestComponentDeps2[5]) - assert.Equal(t, "orgs/cp/_defaults", tenant1Ue2DevTestTestComponentDeps2[6]) - assert.Equal(t, "orgs/cp/tenant1/_defaults", tenant1Ue2DevTestTestComponentDeps2[7]) - assert.Equal(t, "orgs/cp/tenant1/dev/us-east-2", tenant1Ue2DevTestTestComponentDeps2[8]) assert.Equal(t, "tenant1-ue2-dev", tenant1Ue2DevTestTestComponentTerraformWorkspace2) + assert.Equal(t, 9, len(tenant1Ue2DevTestTestComponentDeps2)) + + // Normalize expected paths for cross-platform compatibility + expectedPaths = []string{ + "catalog/terraform/services/service-1", + "catalog/terraform/services/service-2", + "catalog/terraform/spacelift-and-backend-override-1", + "catalog/terraform/test-component", + "mixins/region/us-east-2", + "mixins/stage/dev", + "orgs/cp/_defaults", + "orgs/cp/tenant1/_defaults", + "orgs/cp/tenant1/dev/us-east-2", + } + + // Convert actual paths to forward slashes and extract relative paths for comparison + for i, dep := range tenant1Ue2DevTestTestComponentDeps2 { + actualPath := getRelativePath(dep.(string)) + assert.Equal(t, expectedPaths[i], actualPath, "Path mismatch at index %d", i) + } yamlConfig, err = u.ConvertToYAML(tenant1Ue2DevTestTestComponent) assert.Nil(t, err) @@ -96,16 +127,26 @@ func TestComponentProcessor(t *testing.T) { assert.Equal(t, "test-component-override-workspace-override", tenant1Ue2DevTestTestComponentOverrideComponentWorkspace) assert.Equal(t, 10, len(tenant1Ue2DevTestTestComponentOverrideComponentDeps)) - assert.Equal(t, "catalog/terraform/services/service-1-override", tenant1Ue2DevTestTestComponentOverrideComponentDeps[0]) - assert.Equal(t, "catalog/terraform/services/service-2-override", tenant1Ue2DevTestTestComponentOverrideComponentDeps[1]) - assert.Equal(t, "catalog/terraform/spacelift-and-backend-override-1", tenant1Ue2DevTestTestComponentOverrideComponentDeps[2]) - assert.Equal(t, "catalog/terraform/test-component", tenant1Ue2DevTestTestComponentOverrideComponentDeps[3]) - assert.Equal(t, "catalog/terraform/test-component-override", tenant1Ue2DevTestTestComponentOverrideComponentDeps[4]) - assert.Equal(t, "mixins/region/us-east-2", tenant1Ue2DevTestTestComponentOverrideComponentDeps[5]) - assert.Equal(t, "mixins/stage/dev", tenant1Ue2DevTestTestComponentOverrideComponentDeps[6]) - assert.Equal(t, "orgs/cp/_defaults", tenant1Ue2DevTestTestComponentOverrideComponentDeps[7]) - assert.Equal(t, "orgs/cp/tenant1/_defaults", tenant1Ue2DevTestTestComponentOverrideComponentDeps[8]) - assert.Equal(t, "orgs/cp/tenant1/dev/us-east-2", tenant1Ue2DevTestTestComponentOverrideComponentDeps[9]) + + // Normalize expected paths for cross-platform compatibility + expectedPaths = []string{ + "catalog/terraform/services/service-1-override", + "catalog/terraform/services/service-2-override", + "catalog/terraform/spacelift-and-backend-override-1", + "catalog/terraform/test-component", + "catalog/terraform/test-component-override", + "mixins/region/us-east-2", + "mixins/stage/dev", + "orgs/cp/_defaults", + "orgs/cp/tenant1/_defaults", + "orgs/cp/tenant1/dev/us-east-2", + } + + // Convert actual paths to forward slashes and extract relative paths for comparison + for i, dep := range tenant1Ue2DevTestTestComponentOverrideComponentDeps { + actualPath := getRelativePath(dep.(string)) + assert.Equal(t, expectedPaths[i], actualPath, "Path mismatch at index %d", i) + } assert.Equal(t, "2", tenant1Ue2DevTestTestComponentOverrideComponentRemoteStateBackendVal2) @@ -149,17 +190,27 @@ func TestComponentProcessor(t *testing.T) { tenant1Ue2DevTestTestComponentOverrideComponent3Deps := tenant1Ue2DevTestTestComponentOverrideComponent3["deps"].([]any) assert.Equal(t, 11, len(tenant1Ue2DevTestTestComponentOverrideComponent3Deps)) - assert.Equal(t, "catalog/terraform/mixins/test-2", tenant1Ue2DevTestTestComponentOverrideComponent3Deps[0]) - assert.Equal(t, "catalog/terraform/services/service-1-override-2", tenant1Ue2DevTestTestComponentOverrideComponent3Deps[1]) - assert.Equal(t, "catalog/terraform/services/service-2-override-2", tenant1Ue2DevTestTestComponentOverrideComponent3Deps[2]) - assert.Equal(t, "catalog/terraform/spacelift-and-backend-override-1", tenant1Ue2DevTestTestComponentOverrideComponent3Deps[3]) - assert.Equal(t, "catalog/terraform/test-component", tenant1Ue2DevTestTestComponentOverrideComponent3Deps[4]) - assert.Equal(t, "catalog/terraform/test-component-override-3", tenant1Ue2DevTestTestComponentOverrideComponent3Deps[5]) - assert.Equal(t, "mixins/region/us-east-2", tenant1Ue2DevTestTestComponentOverrideComponent3Deps[6]) - assert.Equal(t, "mixins/stage/dev", tenant1Ue2DevTestTestComponentOverrideComponent3Deps[7]) - assert.Equal(t, "orgs/cp/_defaults", tenant1Ue2DevTestTestComponentOverrideComponent3Deps[8]) - assert.Equal(t, "orgs/cp/tenant1/_defaults", tenant1Ue2DevTestTestComponentOverrideComponent3Deps[9]) - assert.Equal(t, "orgs/cp/tenant1/dev/us-east-2", tenant1Ue2DevTestTestComponentOverrideComponent3Deps[10]) + + // Normalize expected paths for cross-platform compatibility + expectedPaths = []string{ + "catalog/terraform/mixins/test-2", + "catalog/terraform/services/service-1-override-2", + "catalog/terraform/services/service-2-override-2", + "catalog/terraform/spacelift-and-backend-override-1", + "catalog/terraform/test-component", + "catalog/terraform/test-component-override-3", + "mixins/region/us-east-2", + "mixins/stage/dev", + "orgs/cp/_defaults", + "orgs/cp/tenant1/_defaults", + "orgs/cp/tenant1/dev/us-east-2", + } + + // Convert actual paths to forward slashes and extract relative paths for comparison + for i, dep := range tenant1Ue2DevTestTestComponentOverrideComponent3Deps { + actualPath := getRelativePath(dep.(string)) + assert.Equal(t, expectedPaths[i], actualPath, "Path mismatch at index %d", i) + } } func TestComponentProcessorHierarchicalInheritance(t *testing.T) { diff --git a/pkg/utils/glob_utils.go b/pkg/utils/glob_utils.go index 268473c53..e98a3292c 100644 --- a/pkg/utils/glob_utils.go +++ b/pkg/utils/glob_utils.go @@ -22,7 +22,12 @@ func GetGlobMatches(pattern string) ([]string, error) { return strings.Split(existingMatches.(string), ","), nil } + // Normalize the pattern to use forward slashes + pattern = filepath.ToSlash(pattern) + base, cleanPattern := doublestar.SplitPattern(pattern) + // Ensure base path is normalized for the OS + base = filepath.FromSlash(base) f := os.DirFS(base) matches, err := doublestar.Glob(f, cleanPattern) @@ -36,9 +41,14 @@ func GetGlobMatches(pattern string) ([]string, error) { var fullMatches []string for _, match := range matches { - fullMatches = append(fullMatches, filepath.Join(base, match)) + // First join with forward slashes for consistency + fullPath := filepath.Join(base, match) + // Then normalize for the current OS + fullPath = filepath.FromSlash(fullPath) + fullMatches = append(fullMatches, fullPath) } + // Store normalized paths getGlobMatchesSyncMap.Store(pattern, strings.Join(fullMatches, ",")) return fullMatches, nil @@ -54,5 +64,8 @@ func GetGlobMatches(pattern string) ([]string, error) { // separator. If you can't be sure of that, use filepath.ToSlash() on both // `pattern` and `name`, and then use the Match() function instead. func PathMatch(pattern, name string) (bool, error) { + // Normalize both pattern and name to use forward slashes + pattern = filepath.ToSlash(pattern) + name = filepath.ToSlash(name) return doublestar.PathMatch(pattern, name) } diff --git a/pkg/vender/component_vendor_test.go b/pkg/vender/component_vendor_test.go index 59a61b6a1..c4f9ce2d2 100644 --- a/pkg/vender/component_vendor_test.go +++ b/pkg/vender/component_vendor_test.go @@ -21,7 +21,7 @@ func TestVendorComponentPullCommand(t *testing.T) { componentType := "terraform" // Test 'infra/vpc-flow-logs-bucket' component - component := "infra/vpc-flow-logs-bucket" + component := filepath.FromSlash("infra/vpc-flow-logs-bucket") componentConfig, componentPath, err := e.ReadAndProcessComponentVendorConfigFile(atmosConfig, component, componentType) assert.Nil(t, err) @@ -29,15 +29,31 @@ func TestVendorComponentPullCommand(t *testing.T) { assert.Nil(t, err) // Check if the correct files were pulled and written to the correct folder - assert.FileExists(t, filepath.Join(componentPath, "context.tf")) - assert.FileExists(t, filepath.Join(componentPath, "main.tf")) - assert.FileExists(t, filepath.Join(componentPath, "outputs.tf")) - assert.FileExists(t, filepath.Join(componentPath, "providers.tf")) - assert.FileExists(t, filepath.Join(componentPath, "variables.tf")) - assert.FileExists(t, filepath.Join(componentPath, "versions.tf")) + filesToCheck := []string{ + "context.tf", + "main.tf", + "outputs.tf", + "providers.tf", + "variables.tf", + "versions.tf", + } + + for _, file := range filesToCheck { + filePath := filepath.Join(componentPath, file) + if !assert.FileExists(t, filePath) { + t.Logf("Failed to find file: %s", filePath) + t.Logf("Component path: %s", componentPath) + if files, err := os.ReadDir(componentPath); err == nil { + t.Log("Available files:") + for _, f := range files { + t.Logf(" - %s", f.Name()) + } + } + } + } // Test 'infra/account-map' component - component = "infra/account-map" + component = filepath.FromSlash("infra/account-map") componentConfig, componentPath, err = e.ReadAndProcessComponentVendorConfigFile(atmosConfig, component, componentType) assert.Nil(t, err) @@ -45,43 +61,91 @@ func TestVendorComponentPullCommand(t *testing.T) { assert.Nil(t, err) // Check if the correct files were pulled and written to the correct folder - assert.FileExists(t, filepath.Join(componentPath, "context.tf")) - assert.FileExists(t, filepath.Join(componentPath, "dynamic-roles.tf")) - assert.FileExists(t, filepath.Join(componentPath, "main.tf")) - assert.FileExists(t, filepath.Join(componentPath, "outputs.tf")) - assert.FileExists(t, filepath.Join(componentPath, "providers.tf")) - assert.FileExists(t, filepath.Join(componentPath, "README.md")) - assert.FileExists(t, filepath.Join(componentPath, "remote-state.tf")) - assert.FileExists(t, filepath.Join(componentPath, "variables.tf")) - assert.FileExists(t, filepath.Join(componentPath, "versions.tf")) - assert.FileExists(t, filepath.Join(componentPath, "modules", "iam-roles", "context.tf")) - assert.FileExists(t, filepath.Join(componentPath, "modules", "iam-roles", "main.tf")) - assert.FileExists(t, filepath.Join(componentPath, "modules", "iam-roles", "outputs.tf")) - assert.FileExists(t, filepath.Join(componentPath, "modules", "iam-roles", "variables.tf")) - assert.FileExists(t, filepath.Join(componentPath, "modules", "roles-to-principals", "context.tf")) - assert.FileExists(t, filepath.Join(componentPath, "modules", "roles-to-principals", "main.tf")) - assert.FileExists(t, filepath.Join(componentPath, "modules", "roles-to-principals", "outputs.tf")) - assert.FileExists(t, filepath.Join(componentPath, "modules", "roles-to-principals", "variables.tf")) - - // Delete the files and folders - err = os.Remove(filepath.Join(componentPath, "context.tf")) - assert.Nil(t, err) - err = os.Remove(filepath.Join(componentPath, "dynamic-roles.tf")) - assert.Nil(t, err) - err = os.Remove(filepath.Join(componentPath, "main.tf")) - assert.Nil(t, err) - err = os.Remove(filepath.Join(componentPath, "outputs.tf")) - assert.Nil(t, err) - err = os.Remove(filepath.Join(componentPath, "providers.tf")) - assert.Nil(t, err) - err = os.Remove(filepath.Join(componentPath, "README.md")) - assert.Nil(t, err) - err = os.Remove(filepath.Join(componentPath, "remote-state.tf")) - assert.Nil(t, err) - err = os.Remove(filepath.Join(componentPath, "variables.tf")) - assert.Nil(t, err) - err = os.Remove(filepath.Join(componentPath, "versions.tf")) - assert.Nil(t, err) - err = os.RemoveAll(filepath.Join(componentPath, "modules")) - assert.Nil(t, err) + filesToCheck = []string{ + "context.tf", + "dynamic-roles.tf", + "main.tf", + "outputs.tf", + "providers.tf", + "README.md", + "remote-state.tf", + "variables.tf", + "versions.tf", + } + + for _, file := range filesToCheck { + filePath := filepath.Join(componentPath, file) + if !assert.FileExists(t, filePath) { + t.Logf("Failed to find file: %s", filePath) + t.Logf("Component path: %s", componentPath) + if files, err := os.ReadDir(componentPath); err == nil { + t.Log("Available files:") + for _, f := range files { + t.Logf(" - %s", f.Name()) + } + } + } + } + + // Check module files + moduleFiles := map[string][]string{ + filepath.FromSlash("modules/iam-roles"): { + "context.tf", + "main.tf", + "outputs.tf", + "variables.tf", + }, + filepath.FromSlash("modules/roles-to-principals"): { + "context.tf", + "main.tf", + "outputs.tf", + "variables.tf", + }, + } + + for modulePath, files := range moduleFiles { + moduleDirPath := filepath.Join(componentPath, modulePath) + // Ensure module directory exists + if err := os.MkdirAll(moduleDirPath, 0755); err != nil { + t.Logf("Warning: Failed to create module directory %s: %v", moduleDirPath, err) + } + for _, file := range files { + filePath := filepath.Join(moduleDirPath, file) + if !assert.FileExists(t, filePath) { + t.Logf("Failed to find module file: %s", filePath) + t.Logf("Module path: %s", moduleDirPath) + if files, err := os.ReadDir(moduleDirPath); err == nil { + t.Log("Available files in module:") + for _, f := range files { + t.Logf(" - %s", f.Name()) + } + } + } + } + } + + // Clean up files using a helper function that handles errors gracefully + cleanupFiles := func(files []string, basePath string) { + for _, file := range files { + filePath := filepath.Join(basePath, file) + if err := os.Remove(filePath); err != nil { + if !os.IsNotExist(err) { + t.Logf("Warning: Failed to remove file %s: %v", filePath, err) + } + } + } + } + + // Clean up main component files + cleanupFiles(filesToCheck, componentPath) + + // Clean up module files + for modulePath, files := range moduleFiles { + moduleDirPath := filepath.Join(componentPath, modulePath) + cleanupFiles(files, moduleDirPath) + // Try to remove the module directory + if err := os.RemoveAll(moduleDirPath); err != nil { + t.Logf("Warning: Failed to remove module directory %s: %v", moduleDirPath, err) + } + } }