Repository: gruntwork-io/terragrunt

Issue:
Using include variables in dependencies.path list fails on terragrunt 0.32
Previously using `include.inputs.my_variable` was allowed in dependencies.path list.

```hcl
include {
  path   = find_in_parent_folders()
  expose = true
}

dependencies {
  paths = [
    include.inputs.rg_dir
  ]
}
```

With the changes done in terragrunt 0.32.X the same code gives the following error:

```
ERRO[0000] Error processing module at '/home/df/source/tg-bug-test/child/terragrunt.hcl'. How this module was found: Terragrunt config file found in a subdirectory of /home/df/source/tg-bug-test/child. Underlying error: /home/df/source/tg-bug-test/child/terragrunt.hcl:12,19-26: Attempt to get attribute from null value; This value is null, so it does not have any attributes., and 1 other diagnostic(s)
ERRO[0000] Unable to determine underlying exit code, so Terragrunt will exit with error code 1
```

The same code works with no error in terragrunt 0.31.11.

Attached I've included a sample test case: [tg-bug-test.zip](https://github.com/gruntwork-io/terragrunt/files/7186277/tg-bug-test.zip)

Hints from issue comments:
@yorinasub17: This is actually the result of fixing another bug as part of the expose, where the dependency fetching interfered with terragrunt stack generation when `expose = true`.

What happened in 0.32 that is related to the use here is that we switched the exposed `include` parsing to also honor [the configuration parsing order](https://terragrunt.gruntwork.io/docs/getting-started/configuration/#configuration-parsing-order). This means that blocks like `dependency` and `dependencies` can only refer to `included` blocks that are in the same level of parsing (in this case, `locals`, `include` and `dependency`/`dependencies`).

Without this fix, when you use `include` with `expose = true` on a config that has a `dependency` block, `terragrunt` crashes saying the dependency out put is not available, because before it was trying to fully parse the included config while it was building the terragrunt dependency tree. So I'm not inclined to revert to the old behavior because it breaks another use case that is critical to DRY-ing terragrunt (`include` expose config that has `dependency` blocks).

There might be some things we can do to allow this kind of use in limited fashion (e.g., one idea is to fully parse an included config if it has not `dependency` or `dependencies` blocks defined), but I'll need to think a bit about what is the best way to do that.

In the meantime, is it possible to workaround this limitation by using `local`? E.g., can you assign `rg_dir` to a `local` and then use that local in the `inputs` and the exposed include (`include.locals.rg_dir` instead of `include.inputs.rg_dir`)?@danielpof: > In the meantime, is it possible to workaround this limitation by using `local`? E.g., can you assign `rg_dir` to a `local` and then use that local in the `inputs` and the exposed include (`include.locals.rg_dir` instead of `include.inputs.rg_dir`)?

Currently we'll stick to 0.31.11 so we wont have to make any changes to our code.

In the long term `local` could be an option depending on the solution you decide to take for this issue.
@yorinasub17: > we wont have to make any changes to our code.

Just to double check, does the `included` config (the root config where `rg_dir` is defined) have `dependency` or `dependencies` block?@yorinasub17: NOTE: #1817 should resolve this issue for you, assuming your root config doesn't have any `dependency` or `dependencies` blocks defined.@danielpof: Hi there, we do indeed use dependencies and even need the dependencies outputs to build the path.

So going by the parsing order means we couldn't just change to use `local` as I original stated since it's not valid to use dependencies in `locals`.

Our use case is having one main infrastructure "repo" with several child tenants "repo"  that receive values trough a bridge shared module that's used for base values on tenant deployments.

The actual code we is something like this: 
```hcl
primary_data_rg_dir  = "${local.geography != "_global" ? local.geo_dir : local.svc_dir}/${dependency.main.outputs.vars[local.geography].data_primary_locality}/resource-group"
failover_data_rg_dir = "${local.geography != "_global" ? local.geo_dir : local.svc_dir}/${dependency.main.outputs.vars[local.geography].data_failover_locality}/resource-group"
```

So the only current workaround I'm seeing is to just "hardcode" the path values that come from the main "repo" dependency to each tenant's `locals` .
@yorinasub17: The proposed fix is merged, but that does not address the use case described above. The core feature request here is to allow constructing `dependency` config_paths based on other `dependency` outputs.

Unfortunately, as it is now, I don't see if there is a nice way to support this without breaking the other use case, which I believe is much more common than this one. I believe this will be a difficult nut to crack, and not sure if we would ever get to it...@danielpof: Just to give an update on our end, we ended up going with local by copying some key values instead of depending on remote state.

With that, we're able to create all paths in the base locals making them usable everywhere.

It does mean any change to those values will require replicating the change to a dozen repos which shouldn't happen often if at all (fingers crossed).@github-actions: This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for raising this issue.

Solution patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index e735bcc7..9b362291 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -29,7 +29,7 @@ jobs:
           command: |
             # We're running this test only on Windows currently to provide a convenient 
             # means of reproducing Terragrunt issues that only occur on that platform
-            go test -v ./... -run TestTerragruntSourceMapDebug -timeout 45m
+            go test -v ./... -run TestWindowsTerragruntSourceMapDebug -timeout 45m
   # We're running unit tests separately from integration tests - with no parallelization.
   # With heavy parallelization coupled with re-use of test fixtures we've witnessed slight
   # instability with the tests. The unit tests are fast to execute, so there is negligible
diff --git a/cli/cli_app.go b/cli/cli_app.go
index aa523a3b..008aa027 100644
--- a/cli/cli_app.go
+++ b/cli/cli_app.go
@@ -462,9 +462,9 @@ func RunTerragrunt(terragruntOptions *options.TerragruntOptions) error {
 		if err := terragruntConfig.RemoteState.GenerateTerraformCode(updatedTerragruntOptions); err != nil {
 			return err
 		}
-	}
-
-	if terragruntConfig.RemoteState != nil {
+	} else if terragruntConfig.RemoteState != nil {
+		// We use else if here because we don't need to check the backend configuration is defined when the remote state
+		// block has a `generate` attribute configured.
 		if err := checkTerraformCodeDefinesBackend(updatedTerragruntOptions, terragruntConfig.RemoteState.Backend); err != nil {
 			return err
 		}
diff --git a/config/config.go b/config/config.go
index f2ac0366..d5c285bd 100644
--- a/config/config.go
+++ b/config/config.go
@@ -2,6 +2,7 @@ package config
 
 import (
 	"fmt"
+	"io/ioutil"
 	"net/url"
 	"os"
 	"path"
@@ -12,6 +13,7 @@ import (
 	"github.com/hashicorp/go-getter"
 	"github.com/hashicorp/hcl/v2"
 	"github.com/hashicorp/hcl/v2/hclparse"
+	"github.com/hashicorp/hcl/v2/hclwrite"
 	"github.com/mitchellh/mapstructure"
 	"github.com/sirupsen/logrus"
 	"github.com/zclconf/go-cty/cty"
@@ -844,9 +846,9 @@ func validateDependencies(terragruntOptions *options.TerragruntOptions, dependen
 		return nil
 	}
 	for _, dependencyPath := range dependencies.Paths {
-		fullPath := dependencyPath
-		if !filepath.IsAbs(dependencyPath) {
-			fullPath = path.Join(terragruntOptions.WorkingDir, dependencyPath)
+		fullPath := filepath.FromSlash(dependencyPath)
+		if !filepath.IsAbs(fullPath) {
+			fullPath = path.Join(terragruntOptions.WorkingDir, fullPath)
 		}
 		if !util.IsDir(fullPath) {
 			missingDependencies = append(missingDependencies, fmt.Sprintf("%s (%s)", dependencyPath, fullPath))
@@ -878,6 +880,30 @@ func validateGenerateBlocks(blocks *[]terragruntGenerateBlock) error {
 	return nil
 }
 
+// configFileHasDependencyBlock statically checks the terrragrunt config file at the given path and checks if it has any
+// dependency or dependencies blocks defined. Note that this does not do any decoding of the blocks, as it is only meant
+// to check for block presence.
+func configFileHasDependencyBlock(configPath string, terragruntOptions *options.TerragruntOptions) (bool, error) {
+	configBytes, err := ioutil.ReadFile(configPath)
+	if err != nil {
+		return false, errors.WithStackTrace(err)
+	}
+
+	// We use hclwrite to parse the config instead of the normal parser because the normal parser doesn't give us an AST
+	// that we can walk and scan, and requires structured data to map against. This makes the parsing strict, so to
+	// avoid weird parsing errors due to missing dependency data, we do a structural scan here.
+	hclFile, diags := hclwrite.ParseConfig(configBytes, configPath, hcl.InitialPos)
+	if diags.HasErrors() {
+		return false, errors.WithStackTrace(diags)
+	}
+	for _, block := range hclFile.Body().Blocks() {
+		if block.Type() == "dependency" || block.Type() == "dependencies" {
+			return true, nil
+		}
+	}
+	return false, nil
+}
+
 // Custom error types
 
 type InvalidArgError string
diff --git a/config/dependency.go b/config/dependency.go
index 37b9374f..71749ed5 100644
--- a/config/dependency.go
+++ b/config/dependency.go
@@ -462,6 +462,20 @@ func cloneTerragruntOptionsForDependencyOutput(terragruntOptions *options.Terrag
 		targetOptions.DownloadDir = downloadDir
 	}
 
+	// Validate and use TerragruntVersionConstraints.TerraformBinary for dependency
+	partialTerragruntConfig, err := PartialParseConfigFile(
+		targetConfig,
+		targetOptions,
+		nil,
+		[]PartialDecodeSectionType{TerragruntVersionConstraints},
+	)
+	if err != nil {
+		return nil, err
+	}
+	if terragruntOptions.TerraformPath == options.TERRAFORM_DEFAULT_PATH && partialTerragruntConfig.TerraformBinary != "" {
+		targetOptions.TerraformPath = partialTerragruntConfig.TerraformBinary
+	}
+
 	// If the Source is set, then we need to recompute it in the context of the target config.
 	if terragruntOptions.Source != "" {
 		// We need the terraform source of the target config to compute the actual source to use
diff --git a/config/include.go b/config/include.go
index c41a0e0e..e1791583 100644
--- a/config/include.go
+++ b/config/include.go
@@ -35,7 +35,59 @@ func parseIncludedConfig(
 		includePath = util.JoinPath(filepath.Dir(terragruntOptions.TerragruntConfigPath), includePath)
 	}
 
-	if len(decodeList) > 0 {
+	// These condition are here to specifically handle the `run-all` command. During any `run-all` call, terragrunt
+	// needs to first build up the dependency graph to know what order to process the modules in. We want to limit users
+	// from creating a dependency between the dependency path for graph generation, and a module output. This is because
+	// the outputs may not be available yet during the graph generation. E.g., consider a completely new deployment and
+	// `terragrunt run-all apply` is called. In this case, the outputs are expected to be materialized while terragrunt
+	// is running `apply` through the graph, but NOT when the dependency graph is first being formulated.
+	//
+	// To support this, we implement the following conditions for when terragrunt can fully parse the included config
+	// (only one needs to be true):
+	// - Included config does NOT have a dependency block.
+	// - Terragrunt is NOT performing a partial parse (which indicates whether or not Terragrunt is building a module
+	//   graph).
+	//
+	// These conditions are sufficient to avoid a situation where dependency block parsing relies on output fetching.
+	// Note that the user does not have to have a dynamic dependency path that directly depends on dependency outputs to
+	// cause this! For example, suppose the user has a dependency path that depends on an included input:
+	//
+	// include "root" {
+	//   path = find_in_parent_folders()
+	//   expose = true
+	// }
+	// dependency "dep" {
+	//   config_path = include.root.inputs.vpc_dir
+	// }
+	//
+	// In this example, the user the vpc_dir input may not directly depend on a dependency. However, what if the root
+	// config had other inputs that depended on a dependency? E.g.:
+	//
+	// inputs = {
+	//   vpc_dir = "../vpc"
+	//   vpc_id  = dependency.vpc.outputs.id
+	// }
+	//
+	// In this situation, terragrunt can not parse the included inputs attribute unless it fetches the `vpc` dependency
+	// outputs. Since the block parsing is transitive, it leads to a situation where terragrunt cannot parse the `dep`
+	// dependency block unless the `vpc` dependency has outputs (since we can't partially parse the `inputs` attribute).
+	// OTOH, if we know the included config has no `dependency` defined, then no matter what attribute is pulled in, we
+	// know that the `dependency` block path will never depend on dependency outputs. Hence, we perform a full
+	// parse of the included config in the graph generation stage only if the included config does NOT have a dependency
+	// block, but resort to a partial parse otherwise.
+	//
+	// NOTE: To make the logic easier to implement, we implement the inverse here, where we check whether the included
+	// config has a dependency block, and if we are in the middle of a partial parse, we perform a partial parse of the
+	// included config.
+	hasDependency, err := configFileHasDependencyBlock(includePath, terragruntOptions)
+	if err != nil {
+		return nil, err
+	}
+	if hasDependency && len(decodeList) > 0 {
+		terragruntOptions.Logger.Warnf(
+			"Included config %s can only be partially parsed during dependency graph formation for run-all command as it has a dependency block.",
+			includePath,
+		)
 		return PartialParseConfigFile(includePath, terragruntOptions, includedConfig, decodeList)
 	}
 	return ParseConfigFile(includePath, terragruntOptions, includedConfig, dependencyOutputs)
@@ -566,9 +618,9 @@ func updateBareIncludeBlock(file *hcl.File, filename string) ([]byte, bool, erro
 		return updateBareIncludeBlockJSON(file.Bytes)
 	}
 
-	hclFile, err := hclwrite.ParseConfig(file.Bytes, filename, hcl.InitialPos)
-	if err != nil {
-		return nil, false, errors.WithStackTrace(err)
+	hclFile, diags := hclwrite.ParseConfig(file.Bytes, filename, hcl.InitialPos)
+	if diags.HasErrors() {
+		return nil, false, errors.WithStackTrace(diags)
 	}
 
 	codeWasUpdated := false
diff --git a/docs/_docs/04_reference/config-blocks-and-attributes.md b/docs/_docs/04_reference/config-blocks-and-attributes.md
index c9ea980c..89b2dad8 100644
--- a/docs/_docs/04_reference/config-blocks-and-attributes.md
+++ b/docs/_docs/04_reference/config-blocks-and-attributes.md
@@ -508,6 +508,105 @@ inputs = {
 }
 ```
 
+**Limitations on accessing exposed config**
+
+In general, you can access all attributes on `include` when they are exposed (e.g., `include.locals`, `include.inputs`,
+etc).
+
+However, to support `run-all`, Terragrunt is unable to expose all attributes when the included config has a `dependency`
+block. To understand this, consider the following example:
+
+_root terragrunt.hcl_
+```hcl
+dependency "vpc" {
+  config_path = "${get_terragrunt_dir()}/../vpc"
+}
+
+inputs = {
+  vpc_name = dependency.vpc.outputs.name
+}
+```
+
+_child terragrunt.hcl_
+```hcl
+include "root" {
+  path   = find_in_parent_folders()
+  expose = true
+}
+
+dependency "alb" {
+  config_path = (
+    include.root.inputs.vpc_name == "mgmt"
+    ? "../alb-public"
+    : "../alb-private"
+  )
+}
+
+input = {
+  alb_id = dependency.alb.outputs.id
+}
+```
+
+In the child `terragrunt.hcl`, the `dependency` path for the `alb` depends on whether the VPC is the `mgmt` VPC or not,
+which is determined by the `dependency.vpc` in the root config. This means that the ourput from `dependency.vpc` must be
+available to parse the `dependency.alb` config.
+
+This causes problems when performing a `run-all apply` operation. During a `run-all` operation, Terragrunt first parses
+all the `dependency` blocks to build a dependency tree of the Terragrunt modules to figure out the order of operations.
+If all the paths are static references, then Terragrunt can determine all the dependency paths before any module has
+been applied. In this case there is no problem even if other config blocks access `dependency`, as by the time
+Terragrunt needs to parse those blocks, the upstream dependencies would have been applied during the `run-all apply`.
+
+However, if those `dependency` blocks depend on upstream dependencies, then there is a problem as Terragrunt would not
+be able to build the dependency tree without the upstream dependencies being applied.
+
+Therefore, to ensure that Terragrunt can build the dependency tree in a `run-all` operation, Terragrunt enforces the
+following limitation to exposed `include` config:
+
+If the included configuration has any `dependency` blocks, only `locals` and `include` are exposed and available to the
+child `include` and `dependency` blocks. There are no restrictions for other blocks in the child config (e.g., you can
+reference `inputs` from the included config in child `inputs`).
+
+Otherwise, if the included config has no `dependency` blocks, there is no restriction on which exposed attributes you
+can access.
+
+For example, the following alternative configuration is valid even if the alb dependency is still accessing the `inputs`
+attribute from the included config:
+
+_root terragrunt.hcl_
+```hcl
+inputs = {
+  vpc_name = "mgmt"
+}
+```
+
+_child terragrunt.hcl_
+```hcl
+include "root" {
+  path   = find_in_parent_folders()
+  expose = true
+}
+
+dependency "vpc" {
+  config_path = "../vpc"
+}
+
+dependency "alb" {
+  config_path = (
+    include.root.inputs.vpc_name == "mgmt"
+    ? "../alb-public"
+    : "../alb-private"
+  )
+}
+
+input = {
+  vpc_name = dependency.vpc.outputs.name
+  alb_id   = dependency.alb.outputs.id
+}
+```
+
+
+
 **What is deep merge?**
 
 When the `merge_strategy` for the `include` block is set to `deep`, Terragrunt will perform a deep merge of the included



Test patch:
diff --git a/test/fixture-include-expose/with-dependency-reference-input/child/main.tf b/test/fixture-include-expose/with-dependency-reference-input/child/main.tf
new file mode 100644
index 00000000..5e6547a1
--- /dev/null
+++ b/test/fixture-include-expose/with-dependency-reference-input/child/main.tf
@@ -0,0 +1,5 @@
+variable "region" {}
+
+output "region" {
+  value = var.region
+}
diff --git a/test/fixture-include-expose/with-dependency-reference-input/child/terragrunt.hcl b/test/fixture-include-expose/with-dependency-reference-input/child/terragrunt.hcl
new file mode 100644
index 00000000..352c9a73
--- /dev/null
+++ b/test/fixture-include-expose/with-dependency-reference-input/child/terragrunt.hcl
@@ -0,0 +1,12 @@
+include "root" {
+  path           = find_in_parent_folders()
+  expose         = true
+}
+
+dependency "dep" {
+  config_path = include.root.inputs.dep_path
+}
+
+inputs = {
+  region = "${include.root.locals.region}-${dependency.dep.outputs.env}"
+}
diff --git a/test/fixture-include-expose/with-dependency-reference-input/dep/main.tf b/test/fixture-include-expose/with-dependency-reference-input/dep/main.tf
new file mode 100644
index 00000000..8a7d11af
--- /dev/null
+++ b/test/fixture-include-expose/with-dependency-reference-input/dep/main.tf
@@ -0,0 +1,3 @@
+output "env" {
+  value = "test"
+}
diff --git a/test/fixture-include-expose/with-dependency-reference-input/dep/terragrunt.hcl b/test/fixture-include-expose/with-dependency-reference-input/dep/terragrunt.hcl
new file mode 100644
index 00000000..bb7b160d
--- /dev/null
+++ b/test/fixture-include-expose/with-dependency-reference-input/dep/terragrunt.hcl
@@ -0,0 +1 @@
+# Intentionally empty
diff --git a/test/fixture-include-expose/with-dependency-reference-input/terragrunt.hcl b/test/fixture-include-expose/with-dependency-reference-input/terragrunt.hcl
new file mode 100644
index 00000000..9049b70a
--- /dev/null
+++ b/test/fixture-include-expose/with-dependency-reference-input/terragrunt.hcl
@@ -0,0 +1,7 @@
+locals {
+  region = "us-west-1"
+}
+
+inputs = {
+  dep_path = "${get_terragrunt_dir()}/../dep"
+}
diff --git a/test/integration_windows_test.go b/test/integration_windows_test.go
index e03489fc..b064edc9 100644
--- a/test/integration_windows_test.go
+++ b/test/integration_windows_test.go
@@ -25,26 +25,41 @@ func TestWindowsLocalWithRelativeExtraArgsWindows(t *testing.T) {
 	runTerragrunt(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir %s", TEST_FIXTURE_LOCAL_RELATIVE_ARGS_WINDOWS_DOWNLOAD_PATH))
 }
 
-// TestTerragruntSourceMapDebug copies the test/fixture-source-map directory to a new Windows path
+// TestWindowsTerragruntSourceMapDebug copies the test/fixture-source-map directory to a new Windows path
 // and then ensures that the TERRAGRUNT_SOURCE_MAP env var can be used to swap out git sources for local modules
 func TestWindowsTerragruntSourceMapDebug(t *testing.T) {
-	fixtureSourceMapPath := "fixture-source-map"
-	cleanupTerraformFolder(t, fixtureSourceMapPath)
-	targetPath := "C:\\test\\infrastructure-modules/"
-	copyEnvironmentToPath(t, fixtureSourceMapPath, targetPath)
-	rootPath := filepath.Join(targetPath, fixtureSourceMapPath)
-
-	os.Setenv(
-		"TERRAGRUNT_SOURCE_MAP",
-		strings.Join(
-			[]string{
-				fmt.Sprintf("git::ssh://git@github.com/gruntwork-io/i-dont-exist.git=%s", targetPath),
-				fmt.Sprintf("git::ssh://git@github.com/gruntwork-io/another-dont-exist.git=%s", targetPath),
-			},
-			",",
-		),
-	)
-	tgPath := filepath.Join(rootPath, "multiple-match")
-	tgArgs := fmt.Sprintf("terragrunt run-all apply -auto-approve --terragrunt-log-level debug --terragrunt-non-interactive --terragrunt-working-dir %s", tgPath)
-	runTerragrunt(t, tgArgs)
+	testCases := []struct {
+		name string
+	}{
+		{
+			name: "multiple-match",
+		},
+		{
+			name: "multiple-with-dependency",
+		},
+	}
+	for _, testCase := range testCases {
+		testCase := testCase
+		t.Run(testCase.name, func(t *testing.T) {
+			fixtureSourceMapPath := "fixture-source-map"
+			cleanupTerraformFolder(t, fixtureSourceMapPath)
+			targetPath := "C:\\test\\infrastructure-modules/"
+			copyEnvironmentToPath(t, fixtureSourceMapPath, targetPath)
+			rootPath := filepath.Join(targetPath, fixtureSourceMapPath)
+
+			os.Setenv(
+				"TERRAGRUNT_SOURCE_MAP",
+				strings.Join(
+					[]string{
+						fmt.Sprintf("git::ssh://git@github.com/gruntwork-io/i-dont-exist.git=%s", targetPath),
+						fmt.Sprintf("git::ssh://git@github.com/gruntwork-io/another-dont-exist.git=%s", targetPath),
+					},
+					",",
+				),
+			)
+			tgPath := filepath.Join(rootPath, testCase.name)
+			tgArgs := fmt.Sprintf("terragrunt run-all apply -auto-approve --terragrunt-log-level debug --terragrunt-non-interactive --terragrunt-working-dir %s", tgPath)
+			runTerragrunt(t, tgArgs)
+		})
+	}
 }