Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,13 @@ var (

// Git-related errors.
ErrGitNotAvailable = errors.New("git must be available and on the PATH")
ErrGitRoot = errors.New("failed to get git repository root")
ErrGitSHA = errors.New("failed to get git SHA")
ErrGitBranch = errors.New("failed to get git branch")
ErrGitRef = errors.New("failed to get git ref")
ErrGitWorktree = errors.New("failed to get git worktree")
ErrDetachedHead = errors.New("git HEAD is detached")
ErrEmptyBranchName = errors.New("git branch name is empty")
ErrInvalidGitPort = errors.New("invalid port number")
ErrSSHKeyUsage = errors.New("error using SSH key")
ErrGitCommandExited = errors.New("git command exited with non-zero status")
Expand Down
25 changes: 24 additions & 1 deletion internal/exec/yaml_func_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"strings"

atmosGit "github.com/cloudposse/atmos/pkg/git"
log "github.com/cloudposse/atmos/pkg/logger"
"github.com/cloudposse/atmos/pkg/perf"
"github.com/cloudposse/atmos/pkg/schema"
Expand Down Expand Up @@ -80,7 +81,8 @@ func processNodesWithContext(
case string:
result, err := processCustomTagsWithContext(atmosConfig, v, currentStack, skip, resolutionCtx, stackInfo)
if err != nil {
log.Debug("Error processing YAML function",
log.Debug(
"Error processing YAML function",
"value", v,
"stack", currentStack,
"error", err.Error(),
Expand Down Expand Up @@ -188,6 +190,27 @@ func processSimpleTags(
}
return res, true, nil
}
if matchesPrefix(input, u.AtmosYamlFuncGitRoot, skip) || matchesPrefix(input, u.AtmosYamlFuncGitRootAlias, skip) {
res, err := atmosGit.ProcessTagRoot(input)
if err != nil {
return nil, true, err
}
return res, true, nil
}
if matchesPrefix(input, u.AtmosYamlFuncGitSha, skip) || matchesPrefix(input, u.AtmosYamlFuncGitRef, skip) {
res, err := atmosGit.ProcessTagSHA(input)
if err != nil {
return nil, true, err
}
return res, true, nil
}
if matchesPrefix(input, u.AtmosYamlFuncGitBranch, skip) {
res, err := atmosGit.ProcessTagBranch(input)
if err != nil {
return nil, true, err
}
return res, true, nil
}
// AWS YAML functions - note these check for exact match since they take no arguments.
if input == u.AtmosYamlFuncAwsAccountID && !skipFunc(skip, u.AtmosYamlFuncAwsAccountID) {
return processTagAwsAccountID(atmosConfig, input, stackInfo), true, nil
Expand Down
76 changes: 76 additions & 0 deletions internal/exec/yaml_func_utils_context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@ package exec

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

git "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

provSource "github.com/cloudposse/atmos/pkg/provisioner/source"
"github.com/cloudposse/atmos/pkg/schema"
u "github.com/cloudposse/atmos/pkg/utils"
)

func TestProcessCustomYamlTagsWithoutContext(t *testing.T) {
Expand Down Expand Up @@ -47,6 +55,39 @@ func TestProcessCustomYamlTagsCreatesContext(t *testing.T) {
assert.NotNil(t, ctx)
}

func TestProcessCustomYamlTagsGitRefSourceVersion(t *testing.T) {
repoDir, expectedSHA := initExecTestGitRepo(t, "feature/test")
t.Chdir(repoDir)
expectedRoot, err := filepath.EvalSymlinks(repoDir)
require.NoError(t, err)

atmosConfig := &schema.AtmosConfiguration{}
stackYaml := `
source:
uri: github.com/my-org/my-repo//components/terraform/test
version: !git.ref
branch: !git.branch
root: !git.root
`

input, err := u.UnmarshalYAMLFromFile[schema.AtmosSectionMapType](atmosConfig, stackYaml, "test.yaml")
require.NoError(t, err)

result, err := ProcessCustomYamlTags(atmosConfig, input, "test-stack", nil, nil)
require.NoError(t, err)

sourceMap, ok := result["source"].(map[string]any)
require.True(t, ok)

sourceSpec, err := provSource.ExtractSource(map[string]any{"source": sourceMap})
require.NoError(t, err)
require.NotNil(t, sourceSpec)

assert.Equal(t, expectedSHA, sourceSpec.Version)
assert.Equal(t, "feature/test", result["branch"])
assert.Equal(t, expectedRoot, result["root"])
}

func TestProcessCustomYamlTagsWithContextParameter(t *testing.T) {
atmosConfig := &schema.AtmosConfiguration{}
ctx := NewResolutionContext()
Expand All @@ -73,6 +114,41 @@ func TestProcessCustomYamlTagsWithContextParameter(t *testing.T) {
assert.Equal(t, 1, len(ctx.CallStack))
}

func initExecTestGitRepo(t *testing.T, branch string) (string, string) {
t.Helper()

repoDir := t.TempDir()
repo, err := git.PlainInit(repoDir, false)
require.NoError(t, err)

worktree, err := repo.Worktree()
require.NoError(t, err)

filePath := filepath.Join(repoDir, "README.md")
require.NoError(t, os.WriteFile(filePath, []byte("test\n"), 0o644))

_, err = worktree.Add("README.md")
require.NoError(t, err)

hash, err := worktree.Commit("initial commit", &git.CommitOptions{
Author: &object.Signature{
Name: "Atmos Test",
Email: "test@example.com",
When: time.Unix(1, 0),
},
})
require.NoError(t, err)

if branch != "" && branch != "master" {
require.NoError(t, worktree.Checkout(&git.CheckoutOptions{
Branch: plumbing.NewBranchReferenceName(branch),
Create: true,
}))
}

return repoDir, hash.String()
}

func TestProcessNodesWithContextNestedMaps(t *testing.T) {
atmosConfig := &schema.AtmosConfiguration{}
ctx := NewResolutionContext()
Expand Down
82 changes: 75 additions & 7 deletions pkg/config/process_yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/spf13/viper"
"go.yaml.in/yaml/v3"

atmosGit "github.com/cloudposse/atmos/pkg/git"
log "github.com/cloudposse/atmos/pkg/logger"
u "github.com/cloudposse/atmos/pkg/utils"
)
Expand Down Expand Up @@ -201,6 +202,10 @@ func hasCustomTag(tag string) bool {
strings.HasPrefix(tag, u.AtmosYamlFuncExec) ||
strings.HasPrefix(tag, u.AtmosYamlFuncInclude) ||
strings.HasPrefix(tag, u.AtmosYamlFuncGitRoot) ||
strings.HasPrefix(tag, u.AtmosYamlFuncGitRootAlias) ||
strings.HasPrefix(tag, u.AtmosYamlFuncGitSha) ||
strings.HasPrefix(tag, u.AtmosYamlFuncGitBranch) ||
strings.HasPrefix(tag, u.AtmosYamlFuncGitRef) ||
strings.HasPrefix(tag, u.AtmosYamlFuncCwd) ||
strings.HasPrefix(tag, u.AtmosYamlFuncRandom)
}
Expand Down Expand Up @@ -264,14 +269,34 @@ func processIncludeTag(nodeTag, nodeValue, strFunc string) (any, error) {

// processGitRootTag processes the !repo-root tag.
func processGitRootTag(strFunc, nodeValue string) (any, error) {
gitRootValue, err := u.ProcessTagGitRoot(strFunc)
gitRootValue, err := atmosGit.ProcessTagRoot(strFunc)
if err != nil {
log.Debug(failedToProcess, functionKey, strFunc, "error", err)
return nil, fmt.Errorf(errorFormat, ErrExecuteYamlFunctions, u.AtmosYamlFuncGitRoot, nodeValue, err)
return nil, fmt.Errorf(errorFormat, ErrExecuteYamlFunctions, strFunc, nodeValue, err)
}
return strings.TrimSpace(gitRootValue), nil
}

// processGitShaTag processes the !git.sha and !git.ref tags.
func processGitShaTag(strFunc, nodeValue string) (any, error) {
gitShaValue, err := atmosGit.ProcessTagSHA(strFunc)
if err != nil {
log.Debug(failedToProcess, functionKey, strFunc, "error", err)
return nil, fmt.Errorf(errorFormat, ErrExecuteYamlFunctions, strFunc, nodeValue, err)
}
return strings.TrimSpace(gitShaValue), nil
}

// processGitBranchTag processes the !git.branch tag.
func processGitBranchTag(strFunc, nodeValue string) (any, error) {
gitBranchValue, err := atmosGit.ProcessTagBranch(strFunc)
if err != nil {
log.Debug(failedToProcess, functionKey, strFunc, "error", err)
return nil, fmt.Errorf(errorFormat, ErrExecuteYamlFunctions, strFunc, nodeValue, err)
}
return strings.TrimSpace(gitBranchValue), nil
}

// processCwdTag processes the !cwd tag.
func processCwdTag(strFunc, nodeValue string) (any, error) {
cwdValue, err := u.ProcessTagCwd(strFunc)
Expand Down Expand Up @@ -304,8 +329,12 @@ func processScalarNodeValue(node *yaml.Node) (any, error) {
return processExecTag(strFunc, node.Value)
case strings.HasPrefix(node.Tag, u.AtmosYamlFuncInclude):
return processIncludeTag(node.Tag, node.Value, strFunc)
case strings.HasPrefix(node.Tag, u.AtmosYamlFuncGitRoot):
case strings.HasPrefix(node.Tag, u.AtmosYamlFuncGitRoot), strings.HasPrefix(node.Tag, u.AtmosYamlFuncGitRootAlias):
return processGitRootTag(strFunc, node.Value)
case strings.HasPrefix(node.Tag, u.AtmosYamlFuncGitSha), strings.HasPrefix(node.Tag, u.AtmosYamlFuncGitRef):
return processGitShaTag(strFunc, node.Value)
case strings.HasPrefix(node.Tag, u.AtmosYamlFuncGitBranch):
return processGitBranchTag(strFunc, node.Value)
case strings.HasPrefix(node.Tag, u.AtmosYamlFuncCwd):
return processCwdTag(strFunc, node.Value)
case strings.HasPrefix(node.Tag, u.AtmosYamlFuncRandom):
Expand Down Expand Up @@ -335,8 +364,12 @@ func processScalarNode(node *yaml.Node, v *viper.Viper, currentPath string) erro
return handleExec(node, v, currentPath)
case strings.HasPrefix(node.Tag, u.AtmosYamlFuncInclude):
return handleInclude(node, v, currentPath)
case strings.HasPrefix(node.Tag, u.AtmosYamlFuncGitRoot):
case strings.HasPrefix(node.Tag, u.AtmosYamlFuncGitRoot), strings.HasPrefix(node.Tag, u.AtmosYamlFuncGitRootAlias):
return handleGitRoot(node, v, currentPath)
case strings.HasPrefix(node.Tag, u.AtmosYamlFuncGitSha), strings.HasPrefix(node.Tag, u.AtmosYamlFuncGitRef):
return handleGitSha(node, v, currentPath)
case strings.HasPrefix(node.Tag, u.AtmosYamlFuncGitBranch):
return handleGitBranch(node, v, currentPath)
case strings.HasPrefix(node.Tag, u.AtmosYamlFuncCwd):
return handleCwd(node, v, currentPath)
case strings.HasPrefix(node.Tag, u.AtmosYamlFuncRandom):
Expand Down Expand Up @@ -397,7 +430,8 @@ func handleInclude(node *yaml.Node, v *viper.Viper, currentPath string) error {
// Set the value in Viper.
v.Set(currentPath, data)
} else {
log.Warn("Invalid value returned from the YAML function",
log.Warn(
"Invalid value returned from the YAML function",
functionKey, strFunc,
"value", includeValue,
)
Expand All @@ -413,10 +447,10 @@ func handleInclude(node *yaml.Node, v *viper.Viper, currentPath string) error {
// If evaluation fails, it returns an error wrapped with ErrExecuteYamlFunctions; if the result is empty it logs a debug warning but still sets the value.
func handleGitRoot(node *yaml.Node, v *viper.Viper, currentPath string) error {
strFunc := fmt.Sprintf(tagValueFormat, node.Tag, node.Value)
gitRootValue, err := u.ProcessTagGitRoot(strFunc)
gitRootValue, err := atmosGit.ProcessTagRoot(strFunc)
if err != nil {
log.Debug(failedToProcess, functionKey, strFunc, "error", err)
return fmt.Errorf(errorFormat, ErrExecuteYamlFunctions, u.AtmosYamlFuncGitRoot, node.Value, err)
return fmt.Errorf(errorFormat, ErrExecuteYamlFunctions, strFunc, node.Value, err)
}
gitRootValue = strings.TrimSpace(gitRootValue)
if gitRootValue == "" {
Expand All @@ -428,6 +462,40 @@ func handleGitRoot(node *yaml.Node, v *viper.Viper, currentPath string) error {
return nil
}

// handleGitSha evaluates a `!git.sha` or `!git.ref` YAML tag and stores the resulting commit SHA into Viper.
func handleGitSha(node *yaml.Node, v *viper.Viper, currentPath string) error {
strFunc := fmt.Sprintf(tagValueFormat, node.Tag, node.Value)
gitShaValue, err := atmosGit.ProcessTagSHA(strFunc)
if err != nil {
log.Debug(failedToProcess, functionKey, strFunc, "error", err)
return fmt.Errorf(errorFormat, ErrExecuteYamlFunctions, strFunc, node.Value, err)
}
gitShaValue = strings.TrimSpace(gitShaValue)
if gitShaValue == "" {
log.Debug(emptyValueWarning, functionKey, strFunc)
}
v.Set(currentPath, gitShaValue)
node.Tag = ""
return nil
}

// handleGitBranch evaluates a `!git.branch` YAML tag and stores the resulting branch name into Viper.
func handleGitBranch(node *yaml.Node, v *viper.Viper, currentPath string) error {
strFunc := fmt.Sprintf(tagValueFormat, node.Tag, node.Value)
gitBranchValue, err := atmosGit.ProcessTagBranch(strFunc)
if err != nil {
log.Debug(failedToProcess, functionKey, strFunc, "error", err)
return fmt.Errorf(errorFormat, ErrExecuteYamlFunctions, strFunc, node.Value, err)
}
gitBranchValue = strings.TrimSpace(gitBranchValue)
if gitBranchValue == "" {
log.Debug(emptyValueWarning, functionKey, strFunc)
}
v.Set(currentPath, gitBranchValue)
node.Tag = ""
return nil
}

// handleCwd evaluates a `!cwd` YAML tag and stores the current working directory string into Viper at the given path.
// If a path argument is provided, it is joined with CWD.
// If evaluation fails, it returns an error wrapped with ErrExecuteYamlFunctions.
Expand Down
64 changes: 64 additions & 0 deletions pkg/config/process_yaml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import (
"reflect"
"strings"
"testing"
"time"

git "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -634,6 +638,31 @@ func TestProcessCwdTag(t *testing.T) {
}
}

func TestPreprocessAtmosYamlFuncGitTags(t *testing.T) {
repoDir, expectedSHA := initConfigTestGitRepo(t, "feature/test")
t.Chdir(repoDir)
expectedRoot, err := filepath.EvalSymlinks(repoDir)
require.NoError(t, err)

v := viper.New()
yamlStr := `
root: !git.root
legacy_root: !repo-root
sha: !git.sha
ref: !git.ref
branch: !git.branch
`

err = preprocessAtmosYamlFunc([]byte(yamlStr), v)
require.NoError(t, err)

assert.Equal(t, expectedRoot, v.GetString("root"))
assert.Equal(t, expectedRoot, v.GetString("legacy_root"))
assert.Equal(t, expectedSHA, v.GetString("sha"))
assert.Equal(t, expectedSHA, v.GetString("ref"))
assert.Equal(t, "feature/test", v.GetString("branch"))
}

func TestHandleCwd(t *testing.T) {
cwd, err := os.Getwd()
require.NoError(t, err)
Expand Down Expand Up @@ -687,6 +716,41 @@ func TestHandleCwd(t *testing.T) {
}
}

func initConfigTestGitRepo(t *testing.T, branch string) (string, string) {
t.Helper()

repoDir := t.TempDir()
repo, err := git.PlainInit(repoDir, false)
require.NoError(t, err)

worktree, err := repo.Worktree()
require.NoError(t, err)

filePath := filepath.Join(repoDir, "README.md")
require.NoError(t, os.WriteFile(filePath, []byte("test\n"), 0o644))

_, err = worktree.Add("README.md")
require.NoError(t, err)

hash, err := worktree.Commit("initial commit", &git.CommitOptions{
Author: &object.Signature{
Name: "Atmos Test",
Email: "test@example.com",
When: time.Unix(1, 0),
},
})
require.NoError(t, err)

if branch != "" && branch != "master" {
require.NoError(t, worktree.Checkout(&git.CheckoutOptions{
Branch: plumbing.NewBranchReferenceName(branch),
Create: true,
}))
}

return repoDir, hash.String()
}

func TestHandleGitRoot(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading
Loading