Repository: gruntwork-io/terragrunt (language: go)

Issue:
Add `no_validation` attribute to `unit` and `stack` blocks
## Describe the enhancement

Add an attribute to the `unit` and `stack` blocks that disables validation for them.

This can be helpful as users are debugging stack generation, and allows for workarounds if users need to leverage the `unit`/`stack` blocks to generate something other than what's intended (though this should generally be avoided).

## Additional context

See [this interesting conversation in the Terragrunt Discord](https://discord.com/channels/1090098780344422490/1359601202761044039).

## PoC (Proof of Concept)

Link to a Proof of Concept if you have one:

- [ ] [PoC]()

Including a PoC can help others understand the feature better and implement it faster.

## RFC Not Needed

- [x] I have evaluated the complexity of this enhancement, and I believe it does not require an RFC.




Discussion / hints:

Fix patch:
diff --git a/config/config_as_cty.go b/config/config_as_cty.go
index 586eea4314..0a2e7ca217 100644
--- a/config/config_as_cty.go
+++ b/config/config_as_cty.go
@@ -792,6 +792,10 @@ func stackToCty(stack *Stack) (cty.Value, error) {
 		output["no_dot_terragrunt_stack"] = goboolToCty(*stack.NoStack)
 	}
 
+	if stack.NoValidation != nil {
+		output["no_validation"] = goboolToCty(*stack.NoValidation)
+	}
+
 	return convertValuesMapToCtyVal(output)
 }
 
@@ -817,6 +821,10 @@ func unitToCty(unit *Unit) (cty.Value, error) {
 		output["no_dot_terragrunt_stack"] = goboolToCty(*unit.NoStack)
 	}
 
+	if unit.NoValidation != nil {
+		output["no_validation"] = goboolToCty(*unit.NoValidation)
+	}
+
 	return convertValuesMapToCtyVal(output)
 }
 
diff --git a/config/stack.go b/config/stack.go
index 50fcce2d66..6750560511 100644
--- a/config/stack.go
+++ b/config/stack.go
@@ -53,20 +53,22 @@ type StackConfig struct {
 
 // Unit represent unit from stack file.
 type Unit struct {
-	NoStack *bool      `hcl:"no_dot_terragrunt_stack,attr"`
-	Values  *cty.Value `hcl:"values,attr"`
-	Name    string     `hcl:",label"`
-	Source  string     `hcl:"source,attr"`
-	Path    string     `hcl:"path,attr"`
+	NoStack      *bool      `hcl:"no_dot_terragrunt_stack,attr"`
+	NoValidation *bool      `hcl:"no_validation,attr"`
+	Values       *cty.Value `hcl:"values,attr"`
+	Name         string     `hcl:",label"`
+	Source       string     `hcl:"source,attr"`
+	Path         string     `hcl:"path,attr"`
 }
 
 // Stack represents the stack block in the configuration.
 type Stack struct {
-	NoStack *bool      `hcl:"no_dot_terragrunt_stack,attr"`
-	Values  *cty.Value `hcl:"values,attr"`
-	Name    string     `hcl:",label"`
-	Source  string     `hcl:"source,attr"`
-	Path    string     `hcl:"path,attr"`
+	NoStack      *bool      `hcl:"no_dot_terragrunt_stack,attr"`
+	NoValidation *bool      `hcl:"no_validation,attr"`
+	Values       *cty.Value `hcl:"values,attr"`
+	Name         string     `hcl:",label"`
+	Source       string     `hcl:"source,attr"`
+	Path         string     `hcl:"path,attr"`
 }
 
 // GenerateStacks generates the stack files.
@@ -375,14 +377,15 @@ func generateUnits(ctx context.Context, opts *options.TerragruntOptions, pool *w
 
 		pool.Submit(func() error {
 			item := componentToProcess{
-				sourceDir: sourceDir,
-				targetDir: targetDir,
-				name:      unitCopy.Name,
-				path:      unitCopy.Path,
-				source:    unitCopy.Source,
-				values:    unitCopy.Values,
-				noStack:   unitCopy.NoStack != nil && *unitCopy.NoStack,
-				kind:      unitKind,
+				sourceDir:    sourceDir,
+				targetDir:    targetDir,
+				name:         unitCopy.Name,
+				path:         unitCopy.Path,
+				source:       unitCopy.Source,
+				values:       unitCopy.Values,
+				noStack:      unitCopy.NoStack != nil && *unitCopy.NoStack,
+				noValidation: unitCopy.NoValidation != nil && *unitCopy.NoValidation,
+				kind:         unitKind,
 			}
 
 			opts.Logger.Infof("Processing unit %s", unitCopy.Name)
@@ -408,14 +411,15 @@ func generateStacks(ctx context.Context, opts *options.TerragruntOptions, pool *
 
 		pool.Submit(func() error {
 			item := componentToProcess{
-				sourceDir: sourceDir,
-				targetDir: targetDir,
-				name:      stackCopy.Name,
-				path:      stackCopy.Path,
-				source:    stackCopy.Source,
-				noStack:   stackCopy.NoStack != nil && *stackCopy.NoStack,
-				values:    stackCopy.Values,
-				kind:      stackKind,
+				sourceDir:    sourceDir,
+				targetDir:    targetDir,
+				name:         stackCopy.Name,
+				path:         stackCopy.Path,
+				source:       stackCopy.Source,
+				noStack:      stackCopy.NoStack != nil && *stackCopy.NoStack,
+				noValidation: stackCopy.NoValidation != nil && *stackCopy.NoValidation,
+				values:       stackCopy.Values,
+				kind:         stackKind,
 			}
 
 			opts.Logger.Infof("Processing stack %s", stackCopy.Name)
@@ -444,14 +448,15 @@ const (
 // It contains information about the source and target directories, the name and path of the item, the source URL or path,
 // and any associated values that need to be processed.
 type componentToProcess struct {
-	values    *cty.Value
-	sourceDir string
-	targetDir string
-	name      string
-	path      string
-	source    string
-	noStack   bool
-	kind      componentKind
+	values       *cty.Value
+	sourceDir    string
+	targetDir    string
+	name         string
+	path         string
+	source       string
+	noStack      bool
+	noValidation bool
+	kind         componentKind
 }
 
 // processComponent copies files from the source directory to the target destination and generates a corresponding values file.
@@ -519,7 +524,21 @@ func processComponent(ctx context.Context, opts *options.TerragruntOptions, cmp
 		)
 	}
 
-	if !cmp.noStack {
+	skipValidation := false
+
+	if cmp.noStack {
+		opts.Logger.Debugf("Skipping validation for %s %s due to no_stack flag", kindStr, cmp.name)
+
+		skipValidation = true
+	}
+
+	if cmp.noValidation {
+		opts.Logger.Debugf("Skipping validation for %s %s due to no_validation flag", kindStr, cmp.name)
+
+		skipValidation = true
+	}
+
+	if !skipValidation {
 		// validate what was copied to the destination, don't do validation for special noStack components
 		expectedFile := DefaultTerragruntConfigPath
 
diff --git a/docs-starlight/src/content/docs/04-reference/01-hcl/02-blocks.mdx b/docs-starlight/src/content/docs/04-reference/01-hcl/02-blocks.mdx
index a6a77c3be9..966e8f3227 100644
--- a/docs-starlight/src/content/docs/04-reference/01-hcl/02-blocks.mdx
+++ b/docs-starlight/src/content/docs/04-reference/01-hcl/02-blocks.mdx
@@ -1674,6 +1674,7 @@ The `unit` block supports the following arguments:
 - `path` (attribute): The relative path where this unit should be deployed within the stack directory (`.terragrunt-stack`). Also take note of the `no_dot_terragrunt_stack` attribute below, which can impact this.
 - `values` (attribute, optional): A map of values that will be passed to the unit as inputs.
 - `no_dot_terragrunt_stack` (attribute, optional): A boolean flag (`true` or `false`). When set to `true`, the unit **will not** be placed inside the `.terragrunt-stack` directory but will instead be generated in the same directory where `terragrunt.stack.hcl` is located. This allows for a **soft adoption** of stacks, making it easier for users to start using `terragrunt.stack.hcl` without modifying existing directory structures, or performing state migrations.
+- `no_validation` (attribute, optional): A boolean flag (`true` or `false`) that controls whether Terragrunt should validate the unit's configuration. When set to `true`, Terragrunt will skip validation checks for this unit.
 
 Example:
 
@@ -1790,6 +1791,7 @@ The `stack` block supports the following arguments:
 - `path` (attribute): The relative path within `.terragrunt-stack` where this stack should be generated.If an absolute path is provided here, Terragrunt will generate the stack in that location, instead of generating it in a path relative to the `.terragrunt-stack` directory. Also take note of the `no_dot_terragrunt_stack` attribute below, which can impact this.
 - `values` (attribute, optional): A map of custom values that can be passed to the stack. These values can be referenced within the stack's configuration files, allowing for customization without modifying the stack source.
 - `no_dot_terragrunt_stack` (attribute, optional): A boolean flag (`true` or `false`). When set to `true`, the stack **will not** be placed inside the `.terragrunt-stack` directory but will instead be generated in the same directory where `terragrunt.stack.hcl` is located. This allows for a **soft adoption** of stacks, making it easier for users to start using `terragrunt.stack.hcl` without modifying existing directory structures, or performing state migrations.
+- `no_validation` (attribute, optional): A boolean flag (`true` or `false`) that controls whether Terragrunt should validate the stack's configuration. When set to `true`, Terragrunt will skip validation checks for this stack.
 
 Example:
 
diff --git a/docs/_docs/04_reference/04-config-blocks-and-attributes.md b/docs/_docs/04_reference/04-config-blocks-and-attributes.md
index 57310a82c9..e971882e4d 100644
--- a/docs/_docs/04_reference/04-config-blocks-and-attributes.md
+++ b/docs/_docs/04_reference/04-config-blocks-and-attributes.md
@@ -1665,6 +1665,7 @@ The `unit` block supports the following arguments:
 - `path` (attribute): The relative path where this unit should be deployed within the stack directory (`.terragrunt-stack`). If an absolute path is provided here, Terragrunt will generate the stack in that location, instead of generating it in a path relative to the `.terragrunt-stack` directory. Also take note of the `no_dot_terragrunt_stack` attribute below, which can impact this.
 - `values` (attribute, optional): A map of values that will be passed to the unit as inputs.
 - `no_dot_terragrunt_stack` (attribute, optional): A boolean flag (`true` or `false`). When set to `true`, the unit **will not** be placed inside the `.terragrunt-stack` directory but will instead be generated in the same directory where `terragrunt.stack.hcl` is located. This allows for a **soft adoption** of stacks, making it easier for users to start using `terragrunt.stack.hcl` without modifying existing directory structures, or performing state migrations.
+- `no_validation` (attribute, optional): A boolean flag (`true` or `false`) that controls whether Terragrunt should validate the unit's configuration. When set to `true`, Terragrunt will skip validation checks for this unit.
 
 Example:
 
@@ -1775,6 +1776,7 @@ The `stack` block supports the following arguments:
 - `path` (attribute): The relative path within `.terragrunt-stack` where this stack should be generated.If an absolute path is provided here, Terragrunt will generate the stack in that location, instead of generating it in a path relative to the `.terragrunt-stack` directory. Also take note of the `no_dot_terragrunt_stack` attribute below, which can impact this.
 - `values` (attribute, optional): A map of custom values that can be passed to the stack. These values can be referenced within the stack's configuration files, allowing for customization without modifying the stack source.
 - `no_dot_terragrunt_stack` (attribute, optional): A boolean flag (`true` or `false`). When set to `true`, the stack **will not** be placed inside the `.terragrunt-stack` directory but will instead be generated in the same directory where `terragrunt.stack.hcl` is located. This allows for a **soft adoption** of stacks, making it easier for users to start using `terragrunt.stack.hcl` without modifying existing directory structures, or performing state migrations.
+- `no_validation` (attribute, optional): A boolean flag (`true` or `false`) that controls whether Terragrunt should validate the stack's configuration. When set to `true`, Terragrunt will skip validation checks for this stack.
 
 Example:
 


Test patch:
diff --git a/test/fixtures/stacks/no-validation/live/terragrunt.stack.hcl b/test/fixtures/stacks/no-validation/live/terragrunt.stack.hcl
new file mode 100644
index 0000000000..2532f668ba
--- /dev/null
+++ b/test/fixtures/stacks/no-validation/live/terragrunt.stack.hcl
@@ -0,0 +1,12 @@
+unit "unit1" {
+  source        = "../units"
+  path          = "unit1"
+  no_validation = true
+}
+
+unit "stack1" {
+  source        = "../stacks"
+  path          = "stack1"
+  no_validation = true
+}
+
diff --git a/test/fixtures/stacks/no-validation/stacks/stack1/terragrunt.stack.hcl b/test/fixtures/stacks/no-validation/stacks/stack1/terragrunt.stack.hcl
new file mode 100644
index 0000000000..b3555a3bf1
--- /dev/null
+++ b/test/fixtures/stacks/no-validation/stacks/stack1/terragrunt.stack.hcl
@@ -0,0 +1,5 @@
+unit "unit2" {
+  source        = "../../../../units"
+  path          = "unit2"
+  no_validation = true
+}
\ No newline at end of file
diff --git a/test/fixtures/stacks/no-validation/units/app1/code/main.tf b/test/fixtures/stacks/no-validation/units/app1/code/main.tf
new file mode 100644
index 0000000000..b6ef90fb3b
--- /dev/null
+++ b/test/fixtures/stacks/no-validation/units/app1/code/main.tf
@@ -0,0 +1,4 @@
+resource "local_file" "file" {
+  content  = "test file"
+  filename = "${path.module}/file.txt"
+}
diff --git a/test/fixtures/stacks/no-validation/units/app1/code/terragrunt.hcl b/test/fixtures/stacks/no-validation/units/app1/code/terragrunt.hcl
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/test/integration_stacks_test.go b/test/integration_stacks_test.go
index 816215a0a9..18b45d2bab 100644
--- a/test/integration_stacks_test.go
+++ b/test/integration_stacks_test.go
@@ -46,6 +46,7 @@ const (
 	testFixtureReadStack                       = "fixtures/stacks/read-stack"
 	testFixtureStackSelfInclude                = "fixtures/stacks/self-include"
 	testFixtureStackNestedOutputs              = "fixtures/stacks/nested-outputs"
+	testFixtureStackNoValidation               = "fixtures/stacks/no-validation"
 )
 
 func TestStacksGenerateBasic(t *testing.T) {
@@ -1195,6 +1196,23 @@ func TestStackNestedOutputs(t *testing.T) {
 	assert.True(t, stackV2Exists, "stack_v2 block should exist")
 }
 
+func TestStacksNoValidation(t *testing.T) {
+	t.Parallel()
+
+	helpers.CleanupTerraformFolder(t, testFixtureStackNoValidation)
+	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackNoValidation)
+	rootPath := util.JoinPath(tmpEnvPath, testFixtureStackNoValidation, "live")
+
+	stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt stack run plan --experiment stacks --non-interactive --working-dir "+rootPath)
+	require.NoError(t, err)
+
+	assert.Contains(t, stderr, "Module ./.terragrunt-stack/stack1/stack1/.terragrunt-stack/unit2/app1/code")
+	assert.Contains(t, stderr, "Module ./.terragrunt-stack/unit1/app1/code")
+
+	assert.Contains(t, stdout, "Plan: 1 to add, 0 to change, 0 to destroy")
+	assert.Contains(t, stdout, "local_file.file will be created")
+}
+
 // check if the stack directory is created and contains files.
 func validateStackDir(t *testing.T, path string) {
 	t.Helper()