Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a3a5faf
fix: improved handling for deprecated configurations
denis256 Oct 29, 2025
ccd74d2
chore: discovery errors parsing simplifications
denis256 Oct 29, 2025
647ca26
parsing errors simplification
denis256 Oct 29, 2025
f373495
chore: simplified error logs
denis256 Oct 29, 2025
d37d3ce
add tf file
denis256 Oct 29, 2025
fcec3bc
strict error update
denis256 Oct 29, 2025
6cd5407
fix: failing tests fixes
denis256 Oct 29, 2025
b6c6911
chore: goimports update
denis256 Oct 29, 2025
cdc4e56
chore: discovery cleanup
denis256 Oct 30, 2025
3833956
Merge branch 'main' into 4983-parse-error
denis256 Nov 4, 2025
8ac1189
double star constant
denis256 Nov 4, 2025
8fb07b5
Merge remote-tracking branch 'origin/main' into 4983-parse-error
denis256 Nov 4, 2025
b99b08a
Merge remote-tracking branch 'origin/4983-parse-error' into 4983-pars…
denis256 Nov 4, 2025
8d0b226
feat: usage of exernal dependencies from discovery
denis256 Nov 4, 2025
b81dd35
bug: fixed popoulation of dependencies
denis256 Nov 4, 2025
47d8df8
chore: naming convention simplification
denis256 Nov 5, 2025
7df7c88
naming conventions update
denis256 Nov 5, 2025
87cdc45
chore: unit resolver fixer
denis256 Nov 5, 2025
0f63a22
chore: simplifed todos
denis256 Nov 5, 2025
35aee69
Merge remote-tracking branch 'origin/main' into 4983-parse-error
denis256 Nov 5, 2025
a387ccb
unit runner updates
denis256 Nov 5, 2025
9f73d93
fix: failing filtering test
denis256 Nov 5, 2025
71ab432
Merge branch 'main' into 4983-parse-error
denis256 Nov 5, 2025
bd601a0
Merge branch 'main' into 4983-parse-error
denis256 Nov 5, 2025
3216b71
chore: path normalization fixes
denis256 Nov 5, 2025
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
8 changes: 8 additions & 0 deletions config/config_partial.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,14 @@ func PartialParseConfigString(ctx *ParsingContext, l log.Logger, configPath, con
func PartialParseConfig(ctx *ParsingContext, l log.Logger, file *hclparse.File, includeFromChild *IncludeConfig) (*TerragruntConfig, error) {
errs := &errors.MultiError{}

// Detect and block deprecated configurations early, before attempting to parse.
// This ensures included configs with deprecated syntax get clear error messages
// instead of cryptic "Could not find Terragrunt configuration settings" errors.
// See: https://github.com/gruntwork-io/terragrunt/issues/4983
if err := DetectDeprecatedConfigurations(ctx, l, file); err != nil {
return nil, err
}

ctx = ctx.WithTrackInclude(nil)

// read unit files and add to context
Expand Down
34 changes: 32 additions & 2 deletions internal/discovery/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package discovery

import (
"context"
stderrs "errors"
"io"
"io/fs"
"os"
Expand Down Expand Up @@ -514,6 +515,18 @@ func Parse(
//nolint: contextcheck
cfg, err = config.PartialParseConfigFile(parsingCtx, l, parseOpts.TerragruntConfigPath, nil)
if err != nil {
// Treat include-only/no-settings configs as non-fatal during discovery when suppression is enabled
if suppressParseErrors && containsNoSettingsError(err) {
l.Debugf("Skipping include-only config during discovery: %s", parseOpts.TerragruntConfigPath)

// Store an empty partial config to avoid nil dereferences in subsequent dependency discovery
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is odd to me. Why don't we just nil check later?

if unit, ok := c.(*component.Unit); ok {
unit.StoreConfig(&config.TerragruntConfig{IsPartial: true})
}

return nil
}

if !suppressParseErrors || cfg == nil {
l.Debugf("Unrecoverable parse error for %s: %s", parseOpts.TerragruntConfigPath, err)

Expand Down Expand Up @@ -1000,9 +1013,12 @@ func (d *Discovery) Discover(

err := dependencyDiscovery.DiscoverAllDependencies(ctx, l, opts)
if err != nil {
l.Warnf("Parsing errors where encountered while discovering dependencies. They were suppressed, and can be found in the debug logs.")
l.Warnf("Suppressed parsing errors discovered while scanning dependencies. See debug logs for details.")

l.Debugf("Errors: %w", err)
// List underlying errors without %w to avoid confusing formatting like `%!w(...)`
for _, e := range errors.UnwrapErrors(err) {
l.Debugf("Parse error: %v", e)
}
}

components = dependencyDiscovery.components
Expand Down Expand Up @@ -1308,3 +1324,17 @@ func (h *hiddenDirMemo) contains(path string) bool {

return false
}

// containsNoSettingsError returns true if the provided error (possibly a joined/wrapped error)
// contains a config-level error indicating there were no Terragrunt configuration settings
// (e.g., include-only file) that should be treated as non-fatal during discovery.
func containsNoSettingsError(err error) bool {
for _, e := range errors.UnwrapErrors(err) {
var target config.CouldNotResolveTerragruntConfigInFileError
if stderrs.As(e, &target) {
return true
}
}

return false
}
8 changes: 4 additions & 4 deletions internal/runner/runnerpool/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func (dr *Controller) Run(ctx context.Context, l log.Logger) error {

for _, e := range readyEntries {
// log debug which entry is running
l.Debugf("Runner Pool Controller: running %s", e.Component.Path)
l.Debugf("Runner Pool Controller: running %s", e.Component.Path())
dr.q.SetEntryStatus(e, queue.StatusRunning)

sem <- struct{}{}
Expand All @@ -133,7 +133,7 @@ func (dr *Controller) Run(ctx context.Context, l log.Logger) error {
unit := dr.unitsMap[ent.Component.Path()]
if unit == nil {
err := errors.Errorf("unit for path %s not found in discovered units", ent.Component.Path())
l.Errorf("Runner Pool Controller: unit for path %s not found in discovered units, skipping execution", ent.Component.Path)
l.Errorf("Runner Pool Controller: unit for path %s not found in discovered units, skipping execution", ent.Component.Path())
dr.q.FailEntry(ent)
results.Store(ent.Component.Path(), err)

Expand All @@ -144,13 +144,13 @@ func (dr *Controller) Run(ctx context.Context, l log.Logger) error {
results.Store(ent.Component.Path(), err)

if err != nil {
l.Debugf("Runner Pool Controller: %s failed", ent.Component.Path)
l.Debugf("Runner Pool Controller: %s failed", ent.Component.Path())
dr.q.FailEntry(ent)

return
}

l.Debugf("Runner Pool Controller: %s succeeded", ent.Component.Path)
l.Debugf("Runner Pool Controller: %s succeeded", ent.Component.Path())
dr.q.SetEntryStatus(ent, queue.StatusSucceeded)
}(e)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Child configuration that includes compcommon with expose = true
# This should trigger the bug where the deprecated syntax in compcommon isn't detected

include "root" {
path = find_in_parent_folders("root.hcl")
expose = true
}

include "compcommon" {
path = find_in_parent_folders("compcommon.hcl")
expose = true
}

terraform {
source = "git::[email protected]:gruntwork-io/terragrunt.git//test/fixtures/download/hello-world"
}

dependency "service" {
config_path = "../dep"
mock_outputs_allowed_terraform_commands = ["validate", "plan"]
mock_outputs = {
some_value = "mock-service-value"
}
mock_outputs_merge_strategy_with_state = "shallow"
}

# Reference the exposed include - this will try to evaluate compcommon
# which contains the deprecated syntax
inputs = {
from_common = try(include.compcommon.inputs.value_from_dep, "fallback")
from_service = dependency.service.outputs.some_value
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Common configuration that uses deprecated dependency.*.inputs.* syntax
# This should trigger the bug when included with expose = true

dependency "dep" {
config_path = "../dep"
mock_outputs_allowed_terraform_commands = ["validate", "plan"]
mock_outputs = {
some_value = "mock-value"
}
mock_outputs_merge_strategy_with_state = "shallow"
}

# Using deprecated syntax - this should be caught but isn't in partial parse
inputs = {
value_from_dep = dependency.dep.inputs.some_value
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Mock dependency configuration
terraform {
source = "git::[email protected]:gruntwork-io/terragrunt.git//test/fixtures/download/hello-world"
}

inputs = {
some_value = "test-value"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Minimal root configuration
locals {
region = "us-east-1"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
locals {
aws_provider_version = "6.15.0"
}

# Generate an AWS provider block
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
provider "aws" {
region = "us-east-1"
}
EOF
}

generate "versions_override" {
path = "versions_override.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "${local.aws_provider_version}"
}
}
}
EOF
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
variable "desired_count" {
description = "The desired count of the ECS Service"
type = map(number)
}

variable "use_api_image" {
description = "Whether to use the API image"
type = map(bool)
}

resource "null_resource" "services_info" {
triggers = {
desired_count = jsonencode(var.desired_count)
use_api_image = jsonencode(var.use_api_image)
}
}

output "desired_count" {
description = "The desired count of the ECS Service"
value = var.desired_count
}

output "use_api_image" {
description = "Whether to use the API image"
value = var.use_api_image
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
include "root" {
path = find_in_parent_folders("root.hcl")
expose = true
}

terraform {
source = "${get_terragrunt_dir()}/."
}

dependency "service-test1" {
config_path = "../services/test1"
mock_outputs_allowed_terraform_commands = ["validate", "plan"]
mock_outputs = {
service_desired_count_initial_value = null
service_name = "FAKE-SERVICE-1"
docker_images = [
{
repository = "123456789012.dkr.ecr.us-east-1.amazonaws.com/test1-FAKE"
}
]
}
mock_outputs_merge_strategy_with_state = "shallow"
}

locals {
repository_url = "123456789012.dkr.ecr.us-east-1.amazonaws.com/api-FAKE"
name = "services-info"
}

inputs = {
desired_count = {
test1 = dependency.service-test1.outputs.service_desired_count_initial_value
}
use_api_image = {
test1 = dependency.service-test1.outputs.docker_images[0].repository == local.repository_url
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
variable "name" {
type = string
}

output "service_name" {
value = var.name
}

output "service_desired_count_initial_value" {
value = 1
}

output "docker_images" {
value = [
{
repository = "123456789012.dkr.ecr.us-east-1.amazonaws.com/${var.name}-FAKE"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
include "root" {
path = find_in_parent_folders("root.hcl")
expose = true
}

terraform {
source = "${get_terragrunt_dir()}/."
}

locals {
name = "service1"
}

inputs = {
name = local.name
}
1 change: 1 addition & 0 deletions test/integration_parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ var knownBadFiles = []string{
"fixtures/hclfmt-errors/invalid-character/terragrunt.hcl",
"fixtures/hclfmt-errors/invalid-key/terragrunt.hcl",
"fixtures/hclvalidate/second/a/terragrunt.hcl",
"fixtures/parsing/exposed-include-with-deprecated-inputs/compcommon.hcl",
"fixtures/scaffold/with-shell-and-hooks/.boilerplate/terragrunt.hcl",
"fixtures/scaffold/with-shell-commands/.boilerplate/terragrunt.hcl",
}
Expand Down
Loading
Loading