Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
e1d7241
fix: handle triple-slash vendor URIs after go-getter v1.7.9 update
osterman Sep 24, 2025
38b3e93
refactor: rename vendor triple-slash test files to be more descriptive
osterman Sep 24, 2025
5ed9378
docs: improve triple-slash pattern documentation
osterman Sep 24, 2025
27e12d8
test: add unit tests for normalizeVendorURI and fix missing test func…
osterman Sep 24, 2025
f0e312f
docs: clarify triple-slash pattern as root of repository
osterman Sep 24, 2025
c51c3e1
fix: normalize vendor URIs to handle triple-slash pattern correctly
osterman Sep 24, 2025
f4bfec5
fix: address linting comment formatting
osterman Sep 24, 2025
80b7742
fix: improve vendor URI normalization for triple-slash patterns
osterman Sep 24, 2025
a857141
Merge branch 'main' into feature/dev-3639-failed-to-pull-vendors-on-l…
osterman Sep 25, 2025
b03ef7d
Merge branch 'main' into feature/dev-3639-failed-to-pull-vendors-on-l…
osterman Sep 25, 2025
32065af
test: address CodeRabbit feedback and improve test coverage
osterman Sep 25, 2025
ea903d5
style: add missing period to comment per linting requirements
osterman Sep 25, 2025
22bafc6
test: update golden snapshots for vendor pull tests with new debug logs
osterman Sep 25, 2025
1f7cf9a
Merge branch 'main' into feature/dev-3639-failed-to-pull-vendors-on-l…
osterman Sep 26, 2025
c7d1082
Add Atmos CLI performance profiling with multiple profile types (#1534)
aknysh Sep 27, 2025
a43ab81
fix: merge commands from all sources preserving precedence hierarchy …
osterman Sep 27, 2025
7c6d4cd
chore(deps): bump github.com/aws/aws-sdk-go-v2/service/ssm (#1539)
dependabot[bot] Sep 28, 2025
8f7cac3
chore(deps): bump github.com/aws/aws-sdk-go-v2/feature/s3/manager (#1…
dependabot[bot] Sep 28, 2025
1e5b7bf
fix: Improve test infrastructure and fix critical environment variabl…
osterman Sep 29, 2025
b5d5313
fix: final linting adjustments after merge resolution
osterman Sep 29, 2025
37b7e00
Merge branch 'main' of https://github.com/cloudposse/atmos into featu…
osterman Sep 29, 2025
caba443
fix: improve Windows path handling with UNC volume preservation and s…
osterman Sep 29, 2025
071e50d
fix: add nolint directive for godot linter issue
osterman Sep 29, 2025
86b75bd
Merge branch 'feature/dev-3639-failed-to-pull-vendors-on-latest-atmos…
osterman Sep 30, 2025
ab9d262
Merge remote-tracking branch 'origin/main' into feature/dev-3639-fail…
osterman Sep 30, 2025
626fe9f
test: remove obsolete UNC path tests after merge
osterman Sep 30, 2025
0fcd54b
test: add YAML-based precondition system for vendor pull tests
osterman Sep 30, 2025
e2fd0d5
Merge branch 'main' into feature/dev-3639-failed-to-pull-vendors-on-l…
osterman Sep 30, 2025
ca2825b
refactor: remove no-op adjustSubdir and restore integration tests
osterman Oct 1, 2025
3e3d2ae
fix: resolve merge conflicts and apply pending changes
osterman Oct 1, 2025
0a4e523
fix: enhance sanitizeImport and restore test best practices
osterman Oct 1, 2025
7ba97f3
fix: add case-insensitive path matching for Windows CI snapshots
osterman Oct 1, 2025
0274681
Merge remote-tracking branch 'origin/main' into feature/dev-3639-fail…
osterman Oct 1, 2025
59a093b
fix: only replace backslashes on Windows in sanitizeOutput
osterman Oct 1, 2025
24abf9a
fix: use regex to replace backslashes in path contexts only
osterman Oct 1, 2025
656daf5
fix: add github_token precondition to slow vendor tests
osterman Oct 1, 2025
e881711
fix: generalize github_token precondition message
osterman Oct 1, 2025
14decd6
refactor: replace heuristic vendor URI parsing with go-getter API
osterman Oct 2, 2025
046f2ad
refactor: extract named helper functions in vendor URI detection
osterman Oct 2, 2025
109bee8
docs: fix PRD to compare against main branch baseline
osterman Oct 2, 2025
6c09ee1
fix: replace broad .Contains() checks with precise URL parsing
osterman Oct 2, 2025
51c1f0f
fix: add SCP-style Git URL detection and security edge case tests
osterman Oct 2, 2025
180a981
docs: clarify needsDoubleSlashDot function logic
osterman Oct 2, 2025
7dd398b
docs: reorganize vendor URL examples by platform and update repos
osterman Oct 2, 2025
9e971ed
docs: add language identifiers to fenced code blocks in PRD
osterman Oct 2, 2025
eae6225
test: add comprehensive coverage for triple-slash normalization helpers
osterman Oct 2, 2025
86a5bf9
fix: correct hasSubdirectoryDelimiter to skip scheme separators
osterman Oct 2, 2025
fa26be5
Merge branch 'main' into feature/dev-3639-failed-to-pull-vendors-on-l…
osterman Oct 2, 2025
201c215
fix: rename profiler-prefixed config errors to appropriate names
osterman Oct 2, 2025
b362f1d
fix: handle URIs ending with // in appendDoubleSlashDot
osterman Oct 2, 2025
b0d429d
fix: use ErrMerge sentinel for config loading error wrapping
osterman Oct 2, 2025
42e1d13
refactor: move import errors to centralized errors package
osterman Oct 2, 2025
774c6f9
refactor: sort resolved import paths for deterministic logging
osterman Oct 2, 2025
767662e
Revert "refactor: sort resolved import paths for deterministic logging"
osterman Oct 2, 2025
065ed3d
refactor: use t.Cleanup and t.Setenv for test teardown
osterman Oct 2, 2025
b8b270b
docs: add PR review thread response instructions to CLAUDE.md
osterman Oct 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions internal/exec/terraform_clean_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ func TestCLITerraformClean(t *testing.T) {
}
}

func verifyFileExists(t *testing.T, files []string) (bool, string) {
for _, file := range files {
if _, err := os.Stat(file); err != nil {
t.Errorf("Reason: Expected file does not exist: %q", file)
return false, file
}
}
return true, ""
}

func verifyFileDeleted(t *testing.T, files []string) (bool, string) {
for _, file := range files {
fileAbs, err := os.Stat(file)
Expand Down
2 changes: 1 addition & 1 deletion internal/exec/vendor_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ func downloadAndInstall(p *pkgAtmosVendor, dryRun bool, atmosConfig *schema.Atmo
if err := p.installer(&tempDir, atmosConfig); err != nil {
return newInstallError(err, p.name)
}

if err := copyToTargetWithPatterns(tempDir, p.targetPath, &p.atmosVendorSource, p.sourceIsLocalFile); err != nil {
return newInstallError(fmt.Errorf("failed to copy package: %w", err), p.name)
}
Expand All @@ -360,7 +361,6 @@ func (p *pkgAtmosVendor) installer(tempDir *string, atmosConfig *schema.AtmosCon
switch p.pkgType {
case pkgTypeRemote:
// Use go-getter to download remote packages

if err := downloader.NewGoGetterDownloader(atmosConfig).Fetch(p.uri, *tempDir, downloader.ClientModeAny, 10*time.Minute); err != nil {
return fmt.Errorf("failed to download package: %w", err)
}
Expand Down
222 changes: 222 additions & 0 deletions internal/exec/vendor_triple_slash_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package exec

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/cloudposse/atmos/tests"
"github.com/spf13/cobra"
)

// TestVendorPullWithTripleSlashPattern tests the vendor pull command with the triple-slash pattern
// which indicates cloning from the root of a repository (e.g., github.com/repo.git///?ref=v1.0).
// This pattern was broken after go-getter v1.7.9 due to changes in subdirectory path handling.
func TestVendorPullWithTripleSlashPattern(t *testing.T) {
// Check for GitHub access with rate limit check
rateLimits := tests.RequireGitHubAccess(t)
if rateLimits != nil && rateLimits.Remaining < 10 {
t.Skipf("Insufficient GitHub API requests remaining (%d). Test may require ~10 requests", rateLimits.Remaining)
}

// Store original environment variables
originalCliPath := os.Getenv("ATMOS_CLI_CONFIG_PATH")
originalBasePath := os.Getenv("ATMOS_BASE_PATH")
originalLogLevel := os.Getenv("ATMOS_LOGS_LEVEL")

// Set debug logging to see what's happening
os.Setenv("ATMOS_LOGS_LEVEL", "Debug")

// Capture the starting working directory
startingDir, err := os.Getwd()
require.NoError(t, err, "Failed to get the current working directory")

defer func() {
// Restore original environment variables
if originalCliPath != "" {
os.Setenv("ATMOS_CLI_CONFIG_PATH", originalCliPath)
} else {
os.Unsetenv("ATMOS_CLI_CONFIG_PATH")
}

if originalBasePath != "" {
os.Setenv("ATMOS_BASE_PATH", originalBasePath)
} else {
os.Unsetenv("ATMOS_BASE_PATH")
}

if originalLogLevel != "" {
os.Setenv("ATMOS_LOGS_LEVEL", originalLogLevel)
} else {
os.Unsetenv("ATMOS_LOGS_LEVEL")
}

// Change back to the original working directory after the test
if err := os.Chdir(startingDir); err != nil {
t.Fatalf("Failed to change back to the starting directory: %v", err)
}
}()

// Define the test directory
testDir := "../../tests/fixtures/scenarios/vendor-triple-slash"

// Change to the test directory
err = os.Chdir(testDir)
require.NoError(t, err, "Failed to change to test directory")

// Set up the command
cmd := &cobra.Command{}
cmd.PersistentFlags().String("base-path", "", "Base path for Atmos project")
cmd.PersistentFlags().StringSlice("config", []string{}, "Paths to configuration file")
cmd.PersistentFlags().StringSlice("config-path", []string{}, "Path to configuration directory")

flags := cmd.Flags()
flags.String("component", "s3-bucket", "")
flags.String("stack", "", "")
flags.String("tags", "", "")
flags.Bool("dry-run", false, "")
flags.Bool("everything", false, "")

// Execute vendor pull command
err = ExecuteVendorPullCommand(cmd, []string{})
require.NoError(t, err, "Vendor pull command should execute without error")

// Check that the target directory was created
targetDir := filepath.Join("components", "terraform", "s3-bucket")
assert.DirExists(t, targetDir, "Target directory should be created")

// Test that the expected files were pulled from the repository
// According to the bug report, these files should be present but are not being pulled
expectedFiles := []string{
// Main terraform files that should match "**/*.tf"
filepath.Join(targetDir, "main.tf"),
filepath.Join(targetDir, "outputs.tf"),
filepath.Join(targetDir, "variables.tf"),
filepath.Join(targetDir, "versions.tf"),

// Documentation files that should match "**/README.md"
filepath.Join(targetDir, "README.md"),

// License file that should match "**/LICENSE"
filepath.Join(targetDir, "LICENSE"),

// Module files that should match "**/modules/**"
// The terraform-aws-s3-bucket repository has modules subdirectory
filepath.Join(targetDir, "modules", "notification", "main.tf"),
filepath.Join(targetDir, "modules", "notification", "variables.tf"),
filepath.Join(targetDir, "modules", "notification", "outputs.tf"),
filepath.Join(targetDir, "modules", "notification", "versions.tf"),
}

// Check that files were actually pulled (not just an empty directory)
// This is the main assertion that should fail based on the bug report
for _, file := range expectedFiles {
assert.FileExists(t, file, "File should be pulled from repository: %s", file)
}

// Clean up: Remove the created directory and its contents
defer func() {
if err := os.RemoveAll(targetDir); err != nil {
t.Logf("Failed to clean up target directory: %v", err)
}
}()
}

// TestVendorPullWithMultipleVendorFiles tests that vendor pull works correctly
// even when there are multiple vendor YAML files in the same directory.
// This could be a potential cause of the issue where the vendor process
// gets confused by multiple configuration files.
func TestVendorPullWithMultipleVendorFiles(t *testing.T) {
// Check for GitHub access with rate limit check
rateLimits := tests.RequireGitHubAccess(t)
if rateLimits != nil && rateLimits.Remaining < 10 {
t.Skipf("Insufficient GitHub API requests remaining (%d). Test may require ~10 requests", rateLimits.Remaining)
}

// Store original environment variables
originalCliPath := os.Getenv("ATMOS_CLI_CONFIG_PATH")
originalBasePath := os.Getenv("ATMOS_BASE_PATH")
originalLogLevel := os.Getenv("ATMOS_LOGS_LEVEL")

// Set debug logging to see what's happening
os.Setenv("ATMOS_LOGS_LEVEL", "Debug")

// Capture the starting working directory
startingDir, err := os.Getwd()
require.NoError(t, err, "Failed to get the current working directory")

defer func() {
// Restore original environment variables
if originalCliPath != "" {
os.Setenv("ATMOS_CLI_CONFIG_PATH", originalCliPath)
} else {
os.Unsetenv("ATMOS_CLI_CONFIG_PATH")
}

if originalBasePath != "" {
os.Setenv("ATMOS_BASE_PATH", originalBasePath)
} else {
os.Unsetenv("ATMOS_BASE_PATH")
}

if originalLogLevel != "" {
os.Setenv("ATMOS_LOGS_LEVEL", originalLogLevel)
} else {
os.Unsetenv("ATMOS_LOGS_LEVEL")
}

// Change back to the original working directory after the test
if err := os.Chdir(startingDir); err != nil {
t.Fatalf("Failed to change back to the starting directory: %v", err)
}
}()

// Define the test directory
testDir := "../../tests/fixtures/scenarios/vendor-triple-slash"

// Change to the test directory
err = os.Chdir(testDir)
require.NoError(t, err, "Failed to change to test directory")

// Verify that multiple vendor files exist in the directory
vendorFiles := []string{"vendor.yaml", "vendor-test.yaml"}
for _, file := range vendorFiles {
assert.FileExists(t, file, "Vendor file should exist: %s", file)
}

// Set up the command
cmd := &cobra.Command{}
cmd.PersistentFlags().String("base-path", "", "Base path for Atmos project")
cmd.PersistentFlags().StringSlice("config", []string{}, "Paths to configuration file")
cmd.PersistentFlags().StringSlice("config-path", []string{}, "Path to configuration directory")

flags := cmd.Flags()
flags.String("component", "", "")
flags.String("stack", "", "")
flags.String("tags", "aws", "")
flags.Bool("dry-run", false, "")
flags.Bool("everything", false, "")

// Execute vendor pull command with tags filter
err = ExecuteVendorPullCommand(cmd, []string{})
require.NoError(t, err, "Vendor pull command should execute without error even with multiple vendor files")

// Check that the s3-bucket component was pulled (it has the 'aws' tag)
targetDir := filepath.Join("components", "terraform", "s3-bucket")
assert.DirExists(t, targetDir, "Target directory for s3-bucket should be created")

// Verify that at least some files were pulled
entries, err := os.ReadDir(targetDir)
assert.NoError(t, err, "Should be able to read target directory")
assert.NotEmpty(t, entries, "Target directory should not be empty - files should have been pulled")

// Clean up: Remove the created directory and its contents
defer func() {
if err := os.RemoveAll(targetDir); err != nil {
t.Logf("Failed to clean up target directory: %v", err)
}
}()
}
58 changes: 58 additions & 0 deletions internal/exec/vendor_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,11 @@ func processAtmosVendorSource(sources []schema.AtmosVendorSource, component stri
return nil, err
}

// Normalize the URI to handle triple-slash pattern (///), which indicates cloning from
// the root of the repository. This pattern broke in go-getter v1.7.9 due to CVE-2025-8959
// security fixes.
uri = normalizeVendorURI(uri)

useOciScheme, useLocalFileSystem, sourceIsLocalFile, err := determineSourceType(&uri, vendorConfigFilePath)
if err != nil {
return nil, err
Expand Down Expand Up @@ -424,6 +429,59 @@ func shouldSkipSource(s *schema.AtmosVendorSource, component string, tags []stri
return (component != "" && s.Component != component) || (len(tags) > 0 && len(lo.Intersect(tags, s.Tags)) == 0)
}

// normalizeVendorURI normalizes vendor source URIs to handle legacy patterns.
// In go-getter syntax, the double-slash (//) is a delimiter between the repository URL
// and the subdirectory path within that repository. A triple-slash (///) indicates that
// the content should be pulled from the root of the repository.
//
// The triple-slash pattern (e.g., "github.com/repo.git///?ref=v1.0.0") was a documented
// way to explicitly vendor from the root of a Git repository. After go-getter v1.7.9
// (introduced in Atmos v1.189.0), this pattern no longer works due to security fixes
// for CVE-2025-8959 that changed subdirectory path handling.
//
// This function converts:
// - "github.com/repo.git///?ref=v1.0.0" -> "github.com/repo.git?ref=v1.0.0" (root of repository)
// - "github.com/repo.git///some/path?ref=v1.0.0" -> "github.com/repo.git//some/path?ref=v1.0.0"
func normalizeVendorURI(uri string) string {
// Don't modify file:/// URIs (valid file scheme with empty host)
if strings.HasPrefix(uri, "file:///") {
return uri
}

// Check if the URI contains the triple slash pattern for Git repos
// The pattern is: repository.git///?query or repository.git///path?query
if !strings.Contains(uri, ".git///") {
return uri
}

// Find the position of .git///
gitTripleSlashPos := strings.Index(uri, ".git///")
if gitTripleSlashPos == -1 {
return uri
}

// Extract parts before and after .git///
const gitSuffixLen = 4 // length of ".git"
const tripleSlashLen = 7 // length of ".git///"
beforePattern := uri[:gitTripleSlashPos+gitSuffixLen] // includes ".git"
afterPattern := uri[gitTripleSlashPos+tripleSlashLen:] // after "///"

// Check what comes after the triple slash
if afterPattern == "" || strings.HasPrefix(afterPattern, "?") {
// Root of repository case: .git///? or .git///
normalized := beforePattern + afterPattern
log.Debug("Normalized vendor URI: removed root repository indicator (///) to clone from repository root",
"original", uri, "normalized", normalized)
return normalized
}

// Path specified after triple slash: .git///path
normalized := beforePattern + "//" + afterPattern
log.Debug("Normalized vendor URI: converted triple slash to double slash",
"original", uri, "normalized", normalized)
return normalized
}

func determineSourceType(uri *string, vendorConfigFilePath string) (bool, bool, bool, error) {
// Determine if the URI is an OCI scheme, a local file, or remote
useLocalFileSystem := false
Expand Down
Loading
Loading