Skip to content

Commit 25150a6

Browse files
yhakbardenis256
andauthored
fix: Avoid discovering dependencies if they are disabled (#5119)
* fix: Avoid discovering dependencies if they are disabled * chore: added regression tests for disabled dependencies * chore: added regression test * chore: regression tests --------- Co-authored-by: Denis O <denis.o@linux.com>
1 parent bfcf295 commit 25150a6

File tree

7 files changed

+243
-5
lines changed

7 files changed

+243
-5
lines changed

internal/discovery/discovery.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1377,6 +1377,10 @@ func extractDependencyPaths(cfg *config.TerragruntConfig, component component.Co
13771377
var errs []error
13781378

13791379
for _, dependency := range cfg.TerragruntDependencies {
1380+
if dependency.Enabled != nil && !*dependency.Enabled {
1381+
continue
1382+
}
1383+
13801384
if dependency.ConfigPath.Type() != cty.String {
13811385
errs = append(errs, errors.New("dependency config path is not a string"))
13821386
continue

internal/discovery/discovery_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,3 +926,115 @@ dependency "vpc" {
926926
dbDependencyPaths := dbDependencies.Paths()
927927
assert.Contains(t, dbDependencyPaths, vpcDir, "Db should have vpc as a dependency")
928928
}
929+
930+
func TestDiscoveryDetectsCycle(t *testing.T) {
931+
t.Parallel()
932+
933+
tmpDir := t.TempDir()
934+
935+
fooDir := filepath.Join(tmpDir, "foo")
936+
barDir := filepath.Join(tmpDir, "bar")
937+
938+
testDirs := []string{fooDir, barDir}
939+
for _, dir := range testDirs {
940+
err := os.MkdirAll(dir, 0755)
941+
require.NoError(t, err)
942+
}
943+
944+
// Create terragrunt.hcl files with mutual dependencies
945+
testFiles := map[string]string{
946+
filepath.Join(fooDir, "terragrunt.hcl"): `
947+
dependency "bar" {
948+
config_path = "../bar"
949+
}
950+
`,
951+
filepath.Join(barDir, "terragrunt.hcl"): `
952+
dependency "foo" {
953+
config_path = "../foo"
954+
}
955+
`,
956+
}
957+
958+
for path, content := range testFiles {
959+
err := os.WriteFile(path, []byte(content), 0644)
960+
require.NoError(t, err)
961+
}
962+
963+
opts := options.NewTerragruntOptions()
964+
opts.WorkingDir = tmpDir
965+
opts.RootWorkingDir = tmpDir
966+
967+
l := logger.CreateLogger()
968+
969+
// Discover components with dependency discovery enabled
970+
d := discovery.NewDiscovery(tmpDir).WithDiscoverDependencies()
971+
components, err := d.Discover(t.Context(), l, opts)
972+
require.NoError(t, err, "Discovery should complete even with cycles")
973+
974+
// Verify that a cycle is detected
975+
cycleComponent, cycleErr := components.CycleCheck()
976+
require.Error(t, cycleErr, "Cycle check should detect a cycle between foo and bar")
977+
assert.Contains(t, cycleErr.Error(), "cycle detected", "Error message should mention cycle")
978+
assert.NotNil(t, cycleComponent, "Cycle check should return the component that is part of the cycle")
979+
980+
// Verify both foo and bar are in the discovered components
981+
componentPaths := components.Paths()
982+
assert.Contains(t, componentPaths, fooDir, "Foo should be discovered")
983+
assert.Contains(t, componentPaths, barDir, "Bar should be discovered")
984+
}
985+
986+
func TestDiscoveryDoesntDetectCycleWhenDisabled(t *testing.T) {
987+
t.Parallel()
988+
989+
tmpDir := t.TempDir()
990+
991+
fooDir := filepath.Join(tmpDir, "foo")
992+
barDir := filepath.Join(tmpDir, "bar")
993+
994+
testDirs := []string{fooDir, barDir}
995+
for _, dir := range testDirs {
996+
err := os.MkdirAll(dir, 0755)
997+
require.NoError(t, err)
998+
}
999+
1000+
// Create terragrunt.hcl files with mutual dependencies
1001+
testFiles := map[string]string{
1002+
filepath.Join(fooDir, "terragrunt.hcl"): `
1003+
dependency "bar" {
1004+
config_path = "../bar"
1005+
1006+
enabled = false
1007+
}
1008+
`,
1009+
filepath.Join(barDir, "terragrunt.hcl"): `
1010+
dependency "foo" {
1011+
config_path = "../foo"
1012+
}
1013+
`,
1014+
}
1015+
1016+
for path, content := range testFiles {
1017+
err := os.WriteFile(path, []byte(content), 0644)
1018+
require.NoError(t, err)
1019+
}
1020+
1021+
opts := options.NewTerragruntOptions()
1022+
opts.WorkingDir = tmpDir
1023+
opts.RootWorkingDir = tmpDir
1024+
1025+
l := logger.CreateLogger()
1026+
1027+
// Discover components with dependency discovery enabled
1028+
d := discovery.NewDiscovery(tmpDir).WithDiscoverDependencies()
1029+
components, err := d.Discover(t.Context(), l, opts)
1030+
require.NoError(t, err, "Discovery should complete even with cycles")
1031+
1032+
// Verify that a cycle is detected
1033+
_, cycleErr := components.CycleCheck()
1034+
require.NoError(t, cycleErr, "Cycle check should not detect a cycle between foo and bar")
1035+
1036+
// Verify both foo and bar are in the discovered components
1037+
componentPaths := components.Paths()
1038+
assert.Contains(t, componentPaths, fooDir, "Foo should be discovered")
1039+
assert.Contains(t, componentPaths, barDir, "Bar should be discovered")
1040+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
variable "prefix" {
2+
type = string
3+
default = ""
4+
}
5+
6+
variable "suffix" {
7+
type = string
8+
default = ""
9+
}
10+
11+
variable "separator" {
12+
type = string
13+
default = "-"
14+
}
15+
16+
resource "random_string" "this" {
17+
length = 4
18+
upper = false
19+
special = false
20+
}
21+
22+
output "random_string" {
23+
value = random_string.this.result
24+
}
25+
26+
output "id" {
27+
value = format(
28+
"%s%s%s",
29+
(var.prefix == "" ? "" : format("%s%s", trimsuffix(var.prefix, var.separator), var.separator)),
30+
random_string.this.result,
31+
(var.suffix == "" ? "" : format("%s%s", var.separator, trimprefix(var.suffix, var.separator))),
32+
)
33+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Root configuration file
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
include "root" {
2+
path = find_in_parent_folders("root.hcl")
3+
}
4+
5+
terraform {
6+
source = "../modules/id"
7+
}
8+
9+
inputs = {}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
include "root" {
2+
path = find_in_parent_folders("root.hcl")
3+
}
4+
5+
terraform {
6+
source = "../modules/id"
7+
}
8+
9+
locals {
10+
# Test case: disabled dependency with empty config_path
11+
# This should NOT cause cycle errors - the empty path should be ignored
12+
# because the dependency is disabled
13+
unit_a_path = ""
14+
}
15+
16+
dependency "unit_a" {
17+
config_path = try(local.unit_a_path, "")
18+
19+
enabled = false
20+
21+
mock_outputs = {
22+
random_string = ""
23+
}
24+
25+
mock_outputs_merge_strategy_with_state = "shallow"
26+
mock_outputs_allowed_terraform_commands = ["init", "validate", "destroy"]
27+
}
28+
29+
inputs = {
30+
suffix = try(dependency.unit_a.outputs.random_string, "")
31+
}

test/integration_regressions_test.go

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ import (
1414
)
1515

1616
const (
17-
testFixtureRegressions = "fixtures/regressions"
18-
testFixtureDependencyGenerate = "fixtures/regressions/dependency-generate"
19-
testFixtureDependencyEmptyConfigPath = "fixtures/regressions/dependency-empty-config-path"
20-
testFixtureParsingDeprecated = "fixtures/parsing/exposed-include-with-deprecated-inputs"
21-
testFixtureSensitiveValues = "fixtures/regressions/sensitive-values"
17+
testFixtureRegressions = "fixtures/regressions"
18+
testFixtureDependencyGenerate = "fixtures/regressions/dependency-generate"
19+
testFixtureDependencyEmptyConfigPath = "fixtures/regressions/dependency-empty-config-path"
20+
testFixtureDisabledDependencyEmptyConfigPath = "fixtures/regressions/disabled-dependency-empty-config-path"
21+
testFixtureParsingDeprecated = "fixtures/parsing/exposed-include-with-deprecated-inputs"
22+
testFixtureSensitiveValues = "fixtures/regressions/sensitive-values"
2223
)
2324

2425
func TestNoAutoInit(t *testing.T) {
@@ -371,3 +372,50 @@ func TestSensitiveValues(t *testing.T) {
371372
assert.Equal(t, "25", passwordLengthStr,
372373
"Password length should match dev password")
373374
}
375+
376+
// TestDisabledDependencyEmptyConfigPath_NoCycleError tests that disabled dependencies with empty
377+
// config_path values do not cause cycle detection errors during discovery.
378+
// This is a regression test for issue #4977 where setting enabled = false on a dependency
379+
// with an empty config_path ("") was still causing terragrunt to throw cycle errors.
380+
//
381+
// The expected behavior is that disabled dependencies should be completely ignored during
382+
// dependency graph construction and cycle detection, regardless of their config_path value.
383+
//
384+
// See: https://github.com/gruntwork-io/terragrunt/issues/4977
385+
func TestDisabledDependencyEmptyConfigPath_NoCycleError(t *testing.T) {
386+
t.Parallel()
387+
388+
helpers.CleanupTerraformFolder(t, testFixtureDisabledDependencyEmptyConfigPath)
389+
tmpEnvPath := helpers.CopyEnvironment(t, testFixtureDisabledDependencyEmptyConfigPath)
390+
rootPath := util.JoinPath(tmpEnvPath, testFixtureDisabledDependencyEmptyConfigPath)
391+
helpers.CreateGitRepo(t, rootPath)
392+
393+
unitBPath := util.JoinPath(rootPath, "unit-b")
394+
stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(
395+
t,
396+
"terragrunt plan --non-interactive --working-dir "+unitBPath,
397+
)
398+
399+
require.NoError(t, err, "plan should succeed when disabled dependency has empty config_path")
400+
401+
combinedOutput := stdout + stderr
402+
assert.NotContains(t, combinedOutput, "cycle",
403+
"Should not see cycle detection errors for disabled dependencies")
404+
assert.NotContains(t, combinedOutput, "Cycle detected",
405+
"Should not see 'Cycle detected' error")
406+
407+
assert.NotContains(t, combinedOutput, "has empty config_path",
408+
"Should not see empty config_path error for disabled dependency")
409+
410+
_, runAllStderr, runAllErr := helpers.RunTerragruntCommandWithOutput(
411+
t,
412+
"terragrunt run --all plan --non-interactive --working-dir "+rootPath,
413+
)
414+
415+
require.NoError(t, runAllErr, "run --all plan should succeed")
416+
417+
assert.NotContains(t, runAllStderr, "cycle",
418+
"run --all should not see cycle errors")
419+
assert.NotContains(t, runAllStderr, "dependency graph",
420+
"run --all should not see dependency graph errors")
421+
}

0 commit comments

Comments
 (0)