Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
repos:
- repo: https://github.com/gruntwork-io/pre-commit
rev: v0.1.29
rev: v0.1.29
hooks:
- id: tofu-fmt
exclude: test/fixtures/hclvalidate/valid/.*
- id: goimports
4 changes: 2 additions & 2 deletions docs-starlight/src/data/flags/hcl-validate-inputs.mdx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
name: inputs
description: Validate that all variables are set by the module a unit provisions.
description: Validate that all variables a module requires are set.
type: bool
env:
- TG_INPUTS
---

When enabled, Terragrunt will validate that all variables are set by the module a unit provisions.
When enabled, Terragrunt will validate that all variables a module (provisioned by a unit) requires are set.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Open to input on this, or even removing it. But I definitely don't think the current wording is correct (since we're checking that the unit sets variables consumed by the module, the module doesn't set them).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Would better wording be: "When enabled, Terragrunt will validate that all inputs set by Terragrunt units are valid OpenTofu/Terraform variables consumed by their respective modules"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I feel that only is accurate when using --strict, without --strict you can set inputs that are not consumed / don't exist as variables


Example:

Expand Down
4 changes: 4 additions & 0 deletions test/fixtures/hclvalidate/valid/circular-reference/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
locals {
first = local.second
second = local.first
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// intentionally blank
4 changes: 4 additions & 0 deletions test/fixtures/hclvalidate/valid/invalid-local/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# This terraform code is intentionally invalid
output "out" {
value = local.nonexistent
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// intentionally blank
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
variable "input" {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
inputs = {
input = "value"
}
8 changes: 8 additions & 0 deletions test/fixtures/hclvalidate/valid/var-in-source/main.tf
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was able to make an even more minimal example than the one in #4986

Copy link
Collaborator

Choose a reason for hiding this comment

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

Love the names on these fixtures. Reading the name of the fixture should tell you what the fixture is for.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
locals {
variable_source = "github.com/foo/bar"
}

module "module" {
source = local.variable_source
version = "0.0.0"
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I know empty files seem silly, but in the spirit of "minimal reproduction" I don't think you can get more minimal than empty!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Nothing wrong with it! If you want to be explicit about this, you can leave a one-line comment in the file that it's intentionally empty.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// intentionally blank
8 changes: 8 additions & 0 deletions test/fixtures/hclvalidate/valid/var-in-version/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
locals {
variable_version = "0.0.0"
}

module "module" {
source = "github.com/foo/bar"
version = local.variable_version
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// intentionally blank
41 changes: 41 additions & 0 deletions test/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,47 @@ func TestTerragruntExcludesFile(t *testing.T) {
}
}

func TestHclvalidateValidConfig(t *testing.T) {
t.Parallel()

t.Run("using --all", func(t *testing.T) {
t.Parallel()
helpers.CleanupTerraformFolder(t, testFixtureHclvalidate)
tmpEnvPath := helpers.CopyEnvironment(t, testFixtureHclvalidate)
rootPath := util.JoinPath(tmpEnvPath, testFixtureHclvalidate)

_, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt hcl validate --all --strict --inputs --working-dir "+filepath.Join(rootPath, "valid"))
require.NoError(t, err)
})

t.Run("validate each individually", func(t *testing.T) {
t.Parallel()

helpers.CleanupTerraformFolder(t, testFixtureHclvalidate)
tmpEnvPath := helpers.CopyEnvironment(t, testFixtureHclvalidate)
rootPath := util.JoinPath(tmpEnvPath, testFixtureHclvalidate, "valid")

// Test each subdirectory individually
entries, err := os.ReadDir(rootPath)
require.NoError(t, err)

for _, entry := range entries {
if !entry.IsDir() {
continue
}

subPath := filepath.Join(rootPath, entry.Name())

t.Run(entry.Name(), func(t *testing.T) {
t.Parallel()

_, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt hcl validate --strict --inputs --working-dir "+subPath)
require.NoError(t, err)
})
}
})
}

func TestHclvalidateDiagnostic(t *testing.T) {
t.Parallel()

Expand Down
73 changes: 63 additions & 10 deletions tf/tf.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package tf

import (
"os"
"path/filepath"
"slices"

"github.com/gruntwork-io/terragrunt/internal/errors"
"github.com/hashicorp/terraform-config-inspect/tfconfig"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclparse"
)

const (
Expand Down Expand Up @@ -130,21 +135,69 @@ var (
// ModuleVariables will return all the variables defined in the downloaded terraform modules, taking into
// account all the generated sources. This function will return the required and optional variables separately.
func ModuleVariables(modulePath string) ([]string, []string, error) {
module, diags := tfconfig.LoadModule(modulePath)
if diags.HasErrors() {
return nil, nil, errors.New(diags)
parser := hclparse.NewParser()

files, err := os.ReadDir(modulePath)
if err != nil {
return nil, nil, err
}

hclFiles := []*hcl.File{}
allDiags := hcl.Diagnostics{}

for _, file := range files {
if file.IsDir() {
continue
}

parseFunc := parser.ParseHCLFile

suffix := filepath.Ext(file.Name())

if suffix == ".json" {
parseFunc = parser.ParseJSONFile
}

if !(slices.Contains([]string{".tf", ".tofu", ".json"}, suffix)) {
continue
}

file, parseDiags := parseFunc(filepath.Join(modulePath, file.Name()))

hclFiles = append(hclFiles, file)
allDiags = append(allDiags, parseDiags...)
}

body := hcl.MergeFiles(hclFiles)

varsSchema := &hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{
Type: "variable",
LabelNames: []string{"name"},
},
},
}

required := []string{}
optional := []string{}
varsContent, _, contentDiags := body.PartialContent(varsSchema)
allDiags = append(allDiags, contentDiags...)
optional, required := []string{}, []string{}

for _, b := range varsContent.Blocks {
name := b.Labels[0]
attributes, attrDiags := b.Body.JustAttributes()

for _, variable := range module.Variables {
if variable.Required {
required = append(required, variable.Name)
allDiags = append(allDiags, attrDiags...)
if _, ok := attributes["default"]; ok {
optional = append(optional, name)
} else {
optional = append(optional, variable.Name)
required = append(required, name)
}
}

if allDiags.HasErrors() {
return nil, nil, errors.New(allDiags)
}

return required, optional, nil
}
Loading