Skip to content

Commit adad0cb

Browse files
authored
bug: empty paths handling in dependencies (#5016)
* Added test for handling empty config path * chore: simplified dependency output fetching * chore: empty path handling in dependency
1 parent cbc6785 commit adad0cb

File tree

5 files changed

+73
-2
lines changed

5 files changed

+73
-2
lines changed

config/dependency.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,18 @@ func decodeAndRetrieveOutputs(ctx *ParsingContext, l log.Logger, file *hclparse.
208208
// In normal operation, if a dependency block does not have a `config_path` attribute, decoding returns an error since this attribute is required, but the `hclvalidate` command suppresses decoding errors and this causes a cycle between modules, so we need to filter out dependencies without a defined `config_path`.
209209
decodedDependency.Dependencies = decodedDependency.Dependencies.FilteredWithoutConfigPath()
210210

211+
// Validate that dependency config_path is not an empty string.
212+
// Skip null/unknown values and non-strings (which can appear during partial decode or hclvalidate).
213+
for _, dep := range decodedDependency.Dependencies {
214+
if dep.isDisabled() {
215+
continue
216+
}
217+
218+
if isEmptyKnownString(dep.ConfigPath) {
219+
return nil, fmt.Errorf("dependency %q has empty config_path in %s; set a non-empty config_path or disable the dependency", dep.Name, file.ConfigPath)
220+
}
221+
}
222+
211223
if err := checkForDependencyBlockCycles(ctx, l, file.ConfigPath, decodedDependency); err != nil {
212224
return nil, err
213225
}
@@ -1202,3 +1214,8 @@ func (deps Dependencies) FilteredWithoutConfigPath() Dependencies {
12021214

12031215
return filteredDeps
12041216
}
1217+
1218+
// isEmptyKnownString returns true when v is a fully-known string whose trimmed value is empty.
1219+
func isEmptyKnownString(v cty.Value) bool {
1220+
return v.Type().Equals(cty.String) && v.IsWhollyKnown() && strings.TrimSpace(v.AsString()) == ""
1221+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
terraform {}
2+
3+
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
terraform {
2+
source = "."
3+
}
4+
5+
dependency "ecr_cache" {
6+
config_path = try(values.dependency_path.ecr-cache, "")
7+
# force enabled so error manifests even when empty
8+
enabled = true
9+
10+
mock_outputs = {
11+
token = "mock-token"
12+
}
13+
14+
mock_outputs_merge_strategy_with_state = "shallow"
15+
mock_outputs_allowed_terraform_commands = ["init", "validate", "destroy", "plan"]
16+
}
17+
18+
inputs = {
19+
token = dependency.ecr_cache.outputs.token
20+
}
21+
22+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
unit "consumer" {
3+
path = "consumer"
4+
source = "${get_repo_root()}/_source/units/consumer"
5+
6+
values = {
7+
}
8+
}
9+
10+

test/integration_regressions_test.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ import (
1313
)
1414

1515
const (
16-
testFixtureRegressions = "fixtures/regressions"
17-
testFixtureDependencyGenerate = "fixtures/regressions/dependency-generate"
16+
testFixtureRegressions = "fixtures/regressions"
17+
testFixtureDependencyGenerate = "fixtures/regressions/dependency-generate"
18+
testFixtureDependencyEmptyConfigPath = "fixtures/regressions/dependency-empty-config-path"
1819
)
1920

2021
func TestNoAutoInit(t *testing.T) {
@@ -193,3 +194,21 @@ func TestDependencyOutputInInputsStillWorks(t *testing.T) {
193194
strings.Contains(runAllStderr, "test-token-12345"),
194195
"Token should be passed via inputs")
195196
}
197+
198+
func TestDependencyEmptyConfigPath_ReportsError(t *testing.T) {
199+
t.Parallel()
200+
201+
helpers.CleanupTerraformFolder(t, testFixtureDependencyEmptyConfigPath)
202+
tmpEnvPath := helpers.CopyEnvironment(t, testFixtureDependencyEmptyConfigPath)
203+
gitPath := util.JoinPath(tmpEnvPath, testFixtureDependencyEmptyConfigPath)
204+
helpers.CreateGitRepo(t, gitPath)
205+
206+
// Run directly against the consumer unit to force evaluation of dependency outputs
207+
consumerPath := util.JoinPath(gitPath, "_source", "units", "consumer")
208+
_, stderr, runErr := helpers.RunTerragruntCommandWithOutput(t, "terragrunt plan --non-interactive --working-dir "+consumerPath)
209+
require.Error(t, runErr)
210+
// Accept match in either stderr or the returned error string
211+
if !strings.Contains(stderr, "has empty config_path") && !strings.Contains(runErr.Error(), "has empty config_path") {
212+
t.Fatalf("unexpected error; want empty config_path message, got: %v\nstderr: %s", runErr, stderr)
213+
}
214+
}

0 commit comments

Comments
 (0)