From 355ee9aacc7ec623f499f061c986489a29cce613 Mon Sep 17 00:00:00 2001 From: i4k Date: Tue, 26 Nov 2024 22:51:02 +0000 Subject: [PATCH] fix: change detection of Terragrunt module source. Signed-off-by: i4k --- CHANGELOG.md | 4 +++ stack/manager_test.go | 60 +++++++++++++++++++++++++++++++++++++++ test/git.go | 6 +++- tg/tg_module.go | 17 +++++++++++ tg/tg_module_test.go | 66 ++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 151 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8365947c5..a0d2e542d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ Given a version number `MAJOR.MINOR.PATCH`, we increment the: ## Unreleased +### Fixed + +- Fix Terragrunt modules change detection. + ## v0.11.2 ### Added diff --git a/stack/manager_test.go b/stack/manager_test.go index 8a0517818..309ce586a 100644 --- a/stack/manager_test.go +++ b/stack/manager_test.go @@ -193,6 +193,17 @@ func TestListChangedStacks(t *testing.T) { changed: []string{"/tg-stack"}, }, }, + { + name: "single Terragrunt stack with single local Terraform module changed referenced with abspath", + repobuilder: func(t *testing.T) repository { + t.Helper() + return singleTerragruntStackWithSingleTerraformModuleChangedFromAbsPathSourceRepo(t, "force") + }, + want: listTestResult{ + list: []string{"/tg-stack"}, + changed: []string{"/tg-stack"}, + }, + }, { name: "Terragrunt stack changed due to referenced file changed", repobuilder: terragruntStackChangedDueToReferencedFileChangedRepo, @@ -765,6 +776,55 @@ func singleTerragruntStackWithSingleTerraformModuleChangedRepo(t *testing.T, ena return repo } +func singleTerragruntStackWithSingleTerraformModuleChangedFromAbsPathSourceRepo(t *testing.T, enabledOption string) repository { + repo := singleMergeCommitRepoNoStack(t) + test.WriteFile(t, repo.Dir, "terramate.tm.hcl", Doc( + Block("terramate", + Block("config", + Block("change_detection", + Block("terragrunt", + Str("enabled", enabledOption), + ), + ), + ), + ), + ).String()) + modules := test.Mkdir(t, repo.Dir, "modules") + module1 := test.Mkdir(t, modules, "module1") + + repo.modules = append(repo.modules, module1) + + stack := test.Mkdir(t, repo.Dir, "tg-stack") + root, err := config.LoadRoot(repo.Dir) + assert.NoError(t, err) + createStack(t, root, stack) + + g := test.NewGitWrapper(t, repo.Dir, []string{}) + assert.NoError(t, g.Checkout("testbranch", true), "create branch failed") + + test.WriteFile(t, stack, "terragrunt.hcl", Doc( + Block("terraform", + Str("source", "${get_repo_root()}/modules/module1"), + ), + ).String()) + + test.WriteFile(t, module1, "main.tf", `# empty file`) + + assert.NoError(t, g.Add(repo.Dir), "add files") + assert.NoError(t, g.Commit("files"), "commit files") + + addMergeCommit(t, repo.Dir, "testbranch") + assert.NoError(t, g.DeleteBranch("testbranch"), "delete testbranch") + + // now we branch again and modify the module + assert.NoError(t, g.Checkout("testbranch2", true), "create branch testbranch2 failed") + + test.WriteFile(t, module1, "main.tf", `# changed file`) + assert.NoError(t, g.Add(module1), "add files") + assert.NoError(t, g.Commit("module changed"), "commit files") + return repo +} + func terragruntStackChangedDueToDependencyChangedRepo(t *testing.T) repository { repo := singleMergeCommitRepoNoStack(t) stack := test.Mkdir(t, repo.Dir, "tg-stack") diff --git a/test/git.go b/test/git.go index b98896abf..88634229b 100644 --- a/test/git.go +++ b/test/git.go @@ -4,6 +4,7 @@ package test import ( + "path/filepath" "testing" "github.com/madlambda/spells/assert" @@ -48,7 +49,10 @@ func EmptyRepo(t testing.TB, bare bool) string { gw := NewGitWrapper(t, "", []string{}) repodir := TempDir(t) - err := gw.Init(repodir, DefBranch, bare) + // resolve symlinks + repodir, err := filepath.EvalSymlinks(repodir) + assert.NoError(t, err, "evaluating symlinks") + err = gw.Init(repodir, DefBranch, bare) assert.NoError(t, err, "git init") return repodir diff --git a/tg/tg_module.go b/tg/tg_module.go index 2539ac7a9..9b5b34754 100644 --- a/tg/tg_module.go +++ b/tg/tg_module.go @@ -136,6 +136,23 @@ func ScanModules(rootdir string, dir project.Path, trackDependencies bool) (Modu tgMod := stack.Modules[0] mod.Source = *tgConfig.Terraform.Source dependsOn := map[project.Path]struct{}{} + st, err := os.Lstat(mod.Source) + if err == nil && st.IsDir() { + // if the source is a directory, we assume it is a local module. + if filepath.IsAbs(mod.Source) { + src, err := filepath.EvalSymlinks(mod.Source) + if err != nil { + return nil, errors.E(err, "evaluating symlinks in %q", mod.Source) + } + // we normalize local paths as relative to the module. + // so this is compatible with Terraform module sources. + rel, err := filepath.Rel(cfgOpts.WorkingDir, src) + if err != nil { + return nil, errors.E(err, "normalizing local path %q", mod.Source) + } + mod.Source = rel + } + } for _, path := range mod.DependsOn { dependsOn[path] = struct{}{} } diff --git a/tg/tg_module_test.go b/tg/tg_module_test.go index 20e74cef0..46abf3619 100644 --- a/tg/tg_module_test.go +++ b/tg/tg_module_test.go @@ -450,10 +450,74 @@ func TestTerragruntScanModules(t *testing.T) { }, }, }, + { + name: "local module directory also tracked as dependency", + layout: []string{ + `f:some/dir/terragrunt.hcl:` + Doc( + Block("terraform", + Str("source", "${get_repo_root()}/modules/some"), + ), + Block("include", + Labels("root"), + Expr("path", `find_in_parent_folders()`), + ), + Block("dependency", + Labels("other2"), // other2 declared before other: result must be sorted. + Str("config_path", "../other2/dir"), + ), + Block("dependency", + Labels("other"), + Str("config_path", "../other/dir"), + ), + ).String(), + `f:modules/some/main.tf:# empty file`, + `f:some/other/dir/terragrunt.hcl:` + Doc( + Bool("skip", true), + Block("terraform", + Str("source", "https://some.etc/prj"), + )).String(), + `f:some/other2/dir/terragrunt.hcl:` + Block("terraform", + Str("source", "https://some.etc/prj"), + ).String(), + `f:terragrunt.hcl:` + Doc( + Block("terraform"), + ).String(), + `f:common.tfvars:a = "1"`, + `f:regional.tfvars:b = "2"`, + }, + want: want{ + modules: tg.Modules{ + { + Path: project.NewPath("/some/dir"), + Source: "../../modules/some", + ConfigFile: project.NewPath("/some/dir/terragrunt.hcl"), + DependsOn: project.Paths{ + project.NewPath("/some/other/dir"), + project.NewPath("/some/other2/dir"), + project.NewPath("/terragrunt.hcl"), + }, + After: project.Paths{ + project.NewPath("/some/other/dir"), + project.NewPath("/some/other2/dir"), + }, + }, + { + Path: project.NewPath("/some/other/dir"), + Source: "https://some.etc/prj", + ConfigFile: project.NewPath("/some/other/dir/terragrunt.hcl"), + }, + { + Path: project.NewPath("/some/other2/dir"), + Source: "https://some.etc/prj", + ConfigFile: project.NewPath("/some/other2/dir/terragrunt.hcl"), + }, + }, + }, + }, } { tc := tc t.Run(tc.name, func(t *testing.T) { - s := sandbox.NoGit(t, false) + s := sandbox.New(t) s.BuildTree(tc.layout) basedir := tc.basedir if basedir.String() == "" {