From 063f9559061ab27fad926b9234b3f5e61d08ff07 Mon Sep 17 00:00:00 2001
From: haitham911 <haitham911eg@gmail.com>
Date: Tue, 21 Jan 2025 00:50:34 +0200
Subject: [PATCH] push reset to git reset --soft
 15bad9a7f6409cf5abf3762643ae2cd65c026007

---
 examples/tests/test-vendor/atmos.yaml         |  40 ++++++
 .../test-vendor/test-components/README.md     |  13 ++
 .../tests/test-vendor/test-components/main.tf |   7 +
 .../test-vendor/test-components/outputs.tf    |   4 +
 .../test-vendor/test-components/providers.tf  |   1 +
 .../test-vendor/test-components/variables.tf  |   5 +
 .../test-vendor/test-components/versions.tf   |   5 +
 examples/tests/test-vendor/vendor.yaml        |  43 ++++++
 internal/exec/vendor_component_utils.go       |   9 ++
 internal/exec/vendor_model.go                 |   2 +-
 internal/exec/vendor_utils.go                 | 127 +++++++++++++++++-
 tests/cli_test.go                             |  99 +++++++++++++-
 tests/fixtures/scenarios/complete/vendor.yaml |   2 +-
 tests/test-cases/demo-stacks.yaml             |  60 +++++++++
 14 files changed, 408 insertions(+), 9 deletions(-)
 create mode 100644 examples/tests/test-vendor/atmos.yaml
 create mode 100644 examples/tests/test-vendor/test-components/README.md
 create mode 100644 examples/tests/test-vendor/test-components/main.tf
 create mode 100644 examples/tests/test-vendor/test-components/outputs.tf
 create mode 100644 examples/tests/test-vendor/test-components/providers.tf
 create mode 100644 examples/tests/test-vendor/test-components/variables.tf
 create mode 100644 examples/tests/test-vendor/test-components/versions.tf
 create mode 100644 examples/tests/test-vendor/vendor.yaml

diff --git a/examples/tests/test-vendor/atmos.yaml b/examples/tests/test-vendor/atmos.yaml
new file mode 100644
index 000000000..0f0506e81
--- /dev/null
+++ b/examples/tests/test-vendor/atmos.yaml
@@ -0,0 +1,40 @@
+base_path: "./"
+
+components:
+  terraform:
+    base_path: "components/terraform"
+    apply_auto_approve: false
+    deploy_run_init: true
+    init_run_reconfigure: true
+    auto_generate_backend_file: false
+
+stacks:
+  base_path: "stacks"
+  included_paths:
+    - "deploy/**/*"
+  excluded_paths:
+    - "**/_defaults.yaml"
+  name_pattern: "{stage}"
+
+vendor:  
+  # Single file
+  base_path: "./vendor.yaml"
+  
+  # Directory with multiple files
+  #base_path: "./vendor"
+  
+  # Absolute path
+  #base_path: "vendor.d/vendor1.yaml"
+
+logs:
+  file: "/dev/stderr"
+  level: Info
+
+# Custom CLI commands
+
+# No arguments or flags are required
+commands:
+- name: "test"
+  description: "Run all tests"
+  steps:
+  - atmos vendor pull --everything
diff --git a/examples/tests/test-vendor/test-components/README.md b/examples/tests/test-vendor/test-components/README.md
new file mode 100644
index 000000000..a1b28ebfd
--- /dev/null
+++ b/examples/tests/test-vendor/test-components/README.md
@@ -0,0 +1,13 @@
+# Example Terraform IPinfo Component
+
+This Terraform module retrieves data from the IPinfo API for a specified IP address. If no IP address is specified, it retrieves data for the requester's IP address.
+
+## Usage
+
+### Inputs
+
+- `ip_address` (optional): The IP address to retrieve information for. If not specified, the requester's IP address will be used. The default value is an empty string.
+
+### Outputs
+
+- `metadata`: The data retrieved from IPinfo for the specified IP address, in JSON format.
diff --git a/examples/tests/test-vendor/test-components/main.tf b/examples/tests/test-vendor/test-components/main.tf
new file mode 100644
index 000000000..0e3e9d0b0
--- /dev/null
+++ b/examples/tests/test-vendor/test-components/main.tf
@@ -0,0 +1,7 @@
+data "http" "ipinfo" {
+  url = var.ip_address != "" ? "https://ipinfo.io/${var.ip_address}" : "https://ipinfo.io"
+
+  request_headers = {
+    Accept = "application/json"
+  }
+}
diff --git a/examples/tests/test-vendor/test-components/outputs.tf b/examples/tests/test-vendor/test-components/outputs.tf
new file mode 100644
index 000000000..12bb2bb8d
--- /dev/null
+++ b/examples/tests/test-vendor/test-components/outputs.tf
@@ -0,0 +1,4 @@
+output "metadata" {
+  description = "The data retrieved from IPinfo for the specified IP address"
+  value       = jsondecode(data.http.ipinfo.response_body)
+}
diff --git a/examples/tests/test-vendor/test-components/providers.tf b/examples/tests/test-vendor/test-components/providers.tf
new file mode 100644
index 000000000..0b91fe2aa
--- /dev/null
+++ b/examples/tests/test-vendor/test-components/providers.tf
@@ -0,0 +1 @@
+provider "http" {}
diff --git a/examples/tests/test-vendor/test-components/variables.tf b/examples/tests/test-vendor/test-components/variables.tf
new file mode 100644
index 000000000..85f32544d
--- /dev/null
+++ b/examples/tests/test-vendor/test-components/variables.tf
@@ -0,0 +1,5 @@
+variable "ip_address" {
+  description = "The IP address to retrieve information for (optional)"
+  type        = string
+  default     = ""
+}
diff --git a/examples/tests/test-vendor/test-components/versions.tf b/examples/tests/test-vendor/test-components/versions.tf
new file mode 100644
index 000000000..e2a3d732d
--- /dev/null
+++ b/examples/tests/test-vendor/test-components/versions.tf
@@ -0,0 +1,5 @@
+terraform {
+  required_version = ">= 1.0.0"
+
+  required_providers {}
+}
diff --git a/examples/tests/test-vendor/vendor.yaml b/examples/tests/test-vendor/vendor.yaml
new file mode 100644
index 000000000..258a28e81
--- /dev/null
+++ b/examples/tests/test-vendor/vendor.yaml
@@ -0,0 +1,43 @@
+apiVersion: atmos/v1
+kind: AtmosVendorConfig
+metadata:
+  name: demo-vendoring
+  description: Atmos vendoring manifest for Atmos demo component library
+spec:
+  # Import other vendor manifests, if necessary
+  imports: []
+
+  sources:
+    - component: "github/stargazers"
+      source: "github.com/cloudposse/atmos.git//examples/demo-library/{{ .Component }}?ref={{.Version}}"
+      version: "main"
+      targets:
+        - "components/terraform/{{ .Component }}/{{.Version}}"
+      included_paths:
+        - "**/*.tf"
+        - "**/*.tfvars"
+        - "**/*.md"
+      tags:
+        - demo
+        - github
+
+    - component: "test-components"
+      source: "file:///./test-components"
+      version: "main"
+      targets:
+        - "components/terraform/{{ .Component }}/{{.Version}}"
+      tags:
+        - demo
+
+    - component: "weather"
+      source: "git::https://github.com/cloudposse/atmos.git//examples/demo-library/{{ .Component }}?ref={{.Version}}"
+      version: "main"
+      targets:
+        - "components/terraform/{{ .Component }}/{{.Version}}"
+      tags:
+        - demo
+    - component: "my-vpc1"
+      source: "oci://public.ecr.aws/cloudposse/components/terraform/stable/aws/vpc:{{.Version}}"
+      version: "latest"
+      targets:
+        - "components/terraform/infra/my-vpc1"
diff --git a/internal/exec/vendor_component_utils.go b/internal/exec/vendor_component_utils.go
index 5ca0f4c38..94088bdf1 100644
--- a/internal/exec/vendor_component_utils.go
+++ b/internal/exec/vendor_component_utils.go
@@ -5,6 +5,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"net/url"
 	"os"
 	"path/filepath"
 	"strings"
@@ -248,6 +249,14 @@ func ExecuteComponentVendorInternal(
 				sourceIsLocalFile = true
 			}
 		}
+		u, err := url.Parse(uri)
+		if err == nil && u.Scheme != "" {
+			if u.Scheme == "file" {
+				trimmedPath := strings.TrimPrefix(filepath.ToSlash(u.Path), "/")
+				uri = filepath.Clean(trimmedPath)
+				useLocalFileSystem = true
+			}
+		}
 	}
 
 	var pType pkgType
diff --git a/internal/exec/vendor_model.go b/internal/exec/vendor_model.go
index 20e873f24..6ddf14e71 100644
--- a/internal/exec/vendor_model.go
+++ b/internal/exec/vendor_model.go
@@ -248,7 +248,7 @@ func downloadAndInstall(p *pkgAtmosVendor, dryRun bool, atmosConfig schema.Atmos
 			}
 		}
 		// Create temp directory
-		tempDir, err := os.MkdirTemp("", fmt.Sprintf("atmos-vendor-%d-*", time.Now().Unix()))
+		tempDir, err := os.MkdirTemp("", "atmos-vendor")
 		if err != nil {
 			return installedPkgMsg{
 				err:  fmt.Errorf("failed to create temp directory: %w", err),
diff --git a/internal/exec/vendor_utils.go b/internal/exec/vendor_utils.go
index ee6c6520b..6d130931b 100644
--- a/internal/exec/vendor_utils.go
+++ b/internal/exec/vendor_utils.go
@@ -2,6 +2,7 @@ package exec
 
 import (
 	"fmt"
+	"net/url"
 	"os"
 	"path/filepath"
 	"sort"
@@ -9,6 +10,7 @@ import (
 
 	"github.com/bmatcuk/doublestar/v4"
 	tea "github.com/charmbracelet/bubbletea"
+	"github.com/hashicorp/go-getter"
 	cp "github.com/otiai10/copy"
 	"github.com/samber/lo"
 	"github.com/spf13/cobra"
@@ -384,7 +386,7 @@ func ExecuteAtmosVendorInternal(
 			if err != nil {
 				return err
 			}
-			targetPath := filepath.Join(vendorConfigFilePath, target)
+			targetPath := filepath.Join(filepath.ToSlash(vendorConfigFilePath), filepath.ToSlash(target))
 			pkgName := s.Component
 			if pkgName == "" {
 				pkgName = uri
@@ -514,12 +516,22 @@ func determineSourceType(uri *string, vendorConfigFilePath string) (bool, bool,
 	useLocalFileSystem := false
 	sourceIsLocalFile := false
 	if !useOciScheme {
-		if absPath, err := u.JoinAbsolutePathWithPath(vendorConfigFilePath, *uri); err == nil {
+		if absPath, err := u.JoinAbsolutePathWithPath(filepath.ToSlash(vendorConfigFilePath), *uri); err == nil {
 			uri = &absPath
 			useLocalFileSystem = true
 			sourceIsLocalFile = u.FileExists(*uri)
 		}
+		u, err := url.Parse(*uri)
+		if err == nil && u.Scheme != "" {
+			if u.Scheme == "file" {
+				trimmedPath := strings.TrimPrefix(filepath.ToSlash(u.Path), "/")
+				*uri = filepath.Clean(trimmedPath)
+				useLocalFileSystem = true
+			}
+		}
+
 	}
+
 	return useOciScheme, useLocalFileSystem, sourceIsLocalFile
 }
 
@@ -556,6 +568,8 @@ func generateSkipFunction(atmosConfig schema.AtmosConfiguration, tempDir string,
 		if filepath.Base(src) == ".git" {
 			return true, nil
 		}
+		tempDir = filepath.ToSlash(tempDir)
+		src = filepath.ToSlash(src)
 
 		trimmedSrc := u.TrimBasePathFromPath(tempDir+"/", src)
 
@@ -608,3 +622,112 @@ func generateSkipFunction(atmosConfig schema.AtmosConfiguration, tempDir string,
 		return false, nil
 	}
 }
+
+func validateURI(uri string) error {
+	if uri == "" {
+		return fmt.Errorf("URI cannot be empty")
+	}
+	if strings.Contains(uri, " ") {
+		return fmt.Errorf("URI cannot contain spaces")
+	}
+	// Validate scheme-specific format
+	if strings.HasPrefix(uri, "oci://") {
+		if !strings.Contains(uri[6:], "/") {
+			return fmt.Errorf("invalid OCI URI format")
+		}
+	}
+	return nil
+}
+func isValidScheme(scheme string) bool {
+	validSchemes := map[string]bool{
+		"http":       true,
+		"https":      true,
+		"git":        true,
+		"ssh":        true,
+		"git::https": true,
+	}
+	return validSchemes[scheme]
+}
+
+// CustomGitHubDetector intercepts GitHub URLs and transforms them
+// into something like git::https://<token>@github.com/... so we can
+// do a git-based clone with a token.
+type CustomGitHubDetector struct {
+	AtmosConfig schema.AtmosConfiguration
+}
+
+// Detect implements the getter.Detector interface for go-getter v1.
+func (d *CustomGitHubDetector) Detect(src, _ string) (string, bool, error) {
+	if len(src) == 0 {
+		return "", false, nil
+	}
+
+	if !strings.Contains(src, "://") {
+		src = "https://" + src
+	}
+
+	parsedURL, err := url.Parse(src)
+	if err != nil {
+		u.LogDebug(d.AtmosConfig, fmt.Sprintf("Failed to parse URL %q: %v\n", src, err))
+		return "", false, fmt.Errorf("failed to parse URL %q: %w", src, err)
+	}
+
+	if strings.ToLower(parsedURL.Host) != "github.com" {
+		u.LogDebug(d.AtmosConfig, fmt.Sprintf("Host is %q, not 'github.com', skipping token injection\n", parsedURL.Host))
+		return "", false, nil
+	}
+
+	parts := strings.SplitN(parsedURL.Path, "/", 4)
+	if len(parts) < 3 {
+		u.LogDebug(d.AtmosConfig, fmt.Sprintf("URL path %q doesn't look like /owner/repo\n", parsedURL.Path))
+		return "", false, fmt.Errorf("invalid GitHub URL %q", parsedURL.Path)
+	}
+
+	atmosGitHubToken := os.Getenv("ATMOS_GITHUB_TOKEN")
+	gitHubToken := os.Getenv("GITHUB_TOKEN")
+
+	var usedToken string
+	var tokenSource string
+
+	// 1. If ATMOS_GITHUB_TOKEN is set, always use that
+	if atmosGitHubToken != "" {
+		usedToken = atmosGitHubToken
+		tokenSource = "ATMOS_GITHUB_TOKEN"
+		u.LogDebug(d.AtmosConfig, "ATMOS_GITHUB_TOKEN is set\n")
+	} else {
+		// 2. Otherwise, only inject GITHUB_TOKEN if cfg.Settings.InjectGithubToken == true
+		if d.AtmosConfig.Settings.InjectGithubToken && gitHubToken != "" {
+			usedToken = gitHubToken
+			tokenSource = "GITHUB_TOKEN"
+			u.LogTrace(d.AtmosConfig, "InjectGithubToken=true and GITHUB_TOKEN is set, using it\n")
+		} else {
+			u.LogTrace(d.AtmosConfig, "No ATMOS_GITHUB_TOKEN or GITHUB_TOKEN found\n")
+		}
+	}
+
+	if usedToken != "" {
+		user := parsedURL.User.Username()
+		pass, _ := parsedURL.User.Password()
+		if user == "" && pass == "" {
+			u.LogDebug(d.AtmosConfig, fmt.Sprintf("Injecting token from %s for %s\n", tokenSource, src))
+			parsedURL.User = url.UserPassword("x-access-token", usedToken)
+		} else {
+			u.LogDebug(d.AtmosConfig, "Credentials found, skipping token injection\n")
+		}
+	}
+
+	finalURL := "git::" + parsedURL.String()
+
+	return finalURL, true, nil
+}
+
+// RegisterCustomDetectors prepends the custom detector so it runs before
+// the built-in ones. Any code that calls go-getter should invoke this.
+func RegisterCustomDetectors(atmosConfig schema.AtmosConfiguration) {
+	getter.Detectors = append(
+		[]getter.Detector{
+			&CustomGitHubDetector{AtmosConfig: atmosConfig},
+		},
+		getter.Detectors...,
+	)
+}
diff --git a/tests/cli_test.go b/tests/cli_test.go
index 79ed0a494..77c0e2661 100644
--- a/tests/cli_test.go
+++ b/tests/cli_test.go
@@ -5,6 +5,8 @@ import (
 	"errors"
 	"flag"
 	"fmt"
+	"io"
+	"log"
 	"os"
 	"os/exec"
 	"path/filepath" // For resolving absolute paths
@@ -84,7 +86,13 @@ func (m *MatchPattern) UnmarshalYAML(value *yaml.Node) error {
 }
 
 func loadTestSuite(filePath string) (*TestSuite, error) {
-	data, err := os.ReadFile(filePath)
+	// Open the file
+	f, err := os.Open(filePath)
+	if err != nil {
+		log.Fatalf("Failed to open file: %v", err)
+	}
+	defer f.Close()
+	data, err := io.ReadAll(f)
 	if err != nil {
 		return nil, err
 	}
@@ -449,6 +457,76 @@ func TestCLICommands(t *testing.T) {
 		// Run with `t.Run` for non-TTY tests
 		t.Run(tc.Name, func(t *testing.T) {
 			runCLICommandTest(t, tc)
+			defer func() {
+				// 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)
+				}
+			}()
+
+			// Change to the specified working directory
+			if tc.Workdir != "" {
+				err := os.Chdir(tc.Workdir)
+				if err != nil {
+					t.Fatalf("Failed to change directory to %q: %v", tc.Workdir, err)
+				}
+			}
+
+			// Check if the binary exists
+			binaryPath, err := exec.LookPath(tc.Command)
+			if err != nil {
+				t.Fatalf("Binary not found: %s. Current PATH: %s", tc.Command, pathManager.GetPath())
+			}
+
+			// Prepare the command
+			cmd := exec.Command(binaryPath, tc.Args...)
+
+			// Set environment variables
+			envVars := os.Environ()
+			for key, value := range tc.Env {
+				envVars = append(envVars, fmt.Sprintf("%s=%s", key, value))
+			}
+			cmd.Env = envVars
+
+			var stdout, stderr bytes.Buffer
+			cmd.Stdout = &stdout
+			cmd.Stderr = &stderr
+
+			// Run the command
+			err = cmd.Run()
+
+			// Validate exit code
+			exitCode := 0
+			if err != nil {
+				if exitErr, ok := err.(*exec.ExitError); ok {
+					exitCode = exitErr.ExitCode()
+				}
+			}
+			if exitCode != tc.Expect.ExitCode {
+				t.Errorf("Description: %s", tc.Description)
+				t.Errorf("Reason: Expected exit code %d, got %d", tc.Expect.ExitCode, exitCode)
+				t.Errorf("error: %v", cmd.Stderr)
+			}
+
+			// Validate stdout
+			if !verifyOutput(t, "stdout", stdout.String(), tc.Expect.Stdout) {
+				t.Errorf("Description: %s", tc.Description)
+			}
+
+			// Validate stderr
+			if !verifyOutput(t, "stderr", stderr.String(), tc.Expect.Stderr) {
+				t.Errorf("Description: %s", tc.Description)
+			}
+
+			// Validate file existence
+			if !verifyFileExists(t, tc.Expect.FileExists) {
+				t.Errorf("Description: %s", tc.Description)
+			}
+
+			// Validate file contents
+			if !verifyFileContains(t, tc.Expect.FileContains) {
+				t.Errorf("Description: %s", tc.Description)
+			}
 		})
 	}
 }
@@ -512,12 +590,17 @@ func verifyOutput(t *testing.T, outputType, output string, patterns []MatchPatte
 	}
 	return success
 }
-
 func verifyFileExists(t *testing.T, files []string) bool {
 	success := true
 	for _, file := range files {
-		if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) {
-			t.Errorf("Reason: Expected file does not exist: %q", file)
+		fileAbs, err := filepath.Abs(file)
+		if err != nil {
+			log.Println(err)
+			return false
+		}
+
+		if _, err := os.Stat(fileAbs); errors.Is(err, os.ErrNotExist) {
+			t.Errorf("Reason: Expected file does not exist: %q", fileAbs)
 			success = false
 		}
 	}
@@ -527,7 +610,13 @@ func verifyFileExists(t *testing.T, files []string) bool {
 func verifyFileContains(t *testing.T, filePatterns map[string][]MatchPattern) bool {
 	success := true
 	for file, patterns := range filePatterns {
-		content, err := os.ReadFile(file)
+		// Open the file
+		f, err := os.Open(file)
+		if err != nil {
+			log.Fatalf("Failed to open file: %v", err)
+		}
+		defer f.Close()
+		content, err := io.ReadAll(f)
 		if err != nil {
 			t.Errorf("Reason: Failed to read file %q: %v", file, err)
 			success = false
diff --git a/tests/fixtures/scenarios/complete/vendor.yaml b/tests/fixtures/scenarios/complete/vendor.yaml
index 17bba24fc..ab64e00df 100644
--- a/tests/fixtures/scenarios/complete/vendor.yaml
+++ b/tests/fixtures/scenarios/complete/vendor.yaml
@@ -45,7 +45,7 @@ spec:
         - test
         - networking
     - component: "vpc-flow-logs-bucket"
-      source: "github.com/cloudposse/terraform-aws-components.git//modules/vpc-flow-logs-bucket?ref={{.Version}}"
+      source: "git::https://github.com/cloudposse/terraform-aws-components.git//modules/vpc-flow-logs-bucket?ref={{.Version}}"
       version: "1.323.0"
       targets:
         - "components/terraform/infra/vpc-flow-logs-bucket/{{.Version}}"
diff --git a/tests/test-cases/demo-stacks.yaml b/tests/test-cases/demo-stacks.yaml
index f2c7867cc..18a4876ab 100644
--- a/tests/test-cases/demo-stacks.yaml
+++ b/tests/test-cases/demo-stacks.yaml
@@ -130,3 +130,63 @@ tests:
       stderr:
         - "^$"
       exit_code: 0
+
+  - name: atmos greet with args
+    enabled: true
+    description: "Validate atmos custom command greet runs with argument provided."
+    workdir: "../examples/demo-custom-command/"
+    command: "atmos"
+    args:
+      - "greet"
+      - "Andrey"
+    expect:
+      stdout:
+        - "Hello, Andrey\n"
+      stderr:
+        - "^$"
+      exit_code: 0
+
+  - name: atmos greet without args
+    enabled: true
+    description: "Validate atmos custom command greet runs without argument provided."
+    workdir: "../examples/demo-custom-command/"
+    command: "atmos"
+    args:
+      - "greet"
+    expect:
+      stdout:
+        - "Hello, John Doe\n"
+      stderr:
+        - "^$"
+      exit_code: 0
+  - name: atmos vendor pull
+    enabled: true
+    description: "Ensure atmos vendor pull command executes without errors and files are present."
+    workdir: "../examples/tests/test-vendor/"
+    command: "atmos"
+    args:
+      - "vendor"
+      - "pull"
+    expect:
+      file_exists:
+        - "./components/terraform/github/stargazers/main/main.tf"
+        - "./components/terraform/github/stargazers/main/outputs.tf"
+        - "./components/terraform/github/stargazers/main/providers.tf"
+        - "./components/terraform/github/stargazers/main/variables.tf"
+        - "./components/terraform/github/stargazers/main/versions.tf"
+        - "./components/terraform/infra/my-vpc1/main.tf"
+        - "./components/terraform/infra/my-vpc1/outputs.tf"
+        - "./components/terraform/infra/my-vpc1/providers.tf"
+        - "./components/terraform/infra/my-vpc1/variables.tf"
+        - "./components/terraform/infra/my-vpc1/versions.tf"
+        - "./components/terraform/test-components/main/main.tf"
+        - "./components/terraform/test-components/main/outputs.tf"
+        - "./components/terraform/test-components/main/providers.tf"
+        - "./components/terraform/test-components/main/variables.tf"
+        - "./components/terraform/test-components/main/versions.tf"
+        - "./components/terraform/weather/main/main.tf"
+        - "./components/terraform/weather/main/outputs.tf"
+        - "./components/terraform/weather/main/providers.tf"
+        - "./components/terraform/weather/main/variables.tf"
+        - "./components/terraform/weather/main/versions.tf"
+      exit_code: 0