diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 08b9bfaec9..82c6d07589 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/docs-starlight/src/data/flags/hcl-validate-inputs.mdx b/docs-starlight/src/data/flags/hcl-validate-inputs.mdx index 417f631598..0ac286a516 100644 --- a/docs-starlight/src/data/flags/hcl-validate-inputs.mdx +++ b/docs-starlight/src/data/flags/hcl-validate-inputs.mdx @@ -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. Example: diff --git a/test/fixtures/hclvalidate/valid/circular-reference/main.tf b/test/fixtures/hclvalidate/valid/circular-reference/main.tf new file mode 100644 index 0000000000..5cc6b00c80 --- /dev/null +++ b/test/fixtures/hclvalidate/valid/circular-reference/main.tf @@ -0,0 +1,4 @@ +locals { + first = local.second + second = local.first +} diff --git a/test/fixtures/hclvalidate/valid/circular-reference/terragrunt.hcl b/test/fixtures/hclvalidate/valid/circular-reference/terragrunt.hcl new file mode 100644 index 0000000000..65e2cc3404 --- /dev/null +++ b/test/fixtures/hclvalidate/valid/circular-reference/terragrunt.hcl @@ -0,0 +1 @@ +// intentionally blank diff --git a/test/fixtures/hclvalidate/valid/invalid-local/main.tf b/test/fixtures/hclvalidate/valid/invalid-local/main.tf new file mode 100644 index 0000000000..15356f8d69 --- /dev/null +++ b/test/fixtures/hclvalidate/valid/invalid-local/main.tf @@ -0,0 +1,4 @@ +# This terraform code is intentionally invalid +output "out" { + value = local.nonexistent +} diff --git a/test/fixtures/hclvalidate/valid/invalid-local/terragrunt.hcl b/test/fixtures/hclvalidate/valid/invalid-local/terragrunt.hcl new file mode 100644 index 0000000000..65e2cc3404 --- /dev/null +++ b/test/fixtures/hclvalidate/valid/invalid-local/terragrunt.hcl @@ -0,0 +1 @@ +// intentionally blank diff --git a/test/fixtures/hclvalidate/valid/single-required-input/main.tf b/test/fixtures/hclvalidate/valid/single-required-input/main.tf new file mode 100644 index 0000000000..682e0f0db7 --- /dev/null +++ b/test/fixtures/hclvalidate/valid/single-required-input/main.tf @@ -0,0 +1,2 @@ +variable "input" { +} diff --git a/test/fixtures/hclvalidate/valid/single-required-input/terragrunt.hcl b/test/fixtures/hclvalidate/valid/single-required-input/terragrunt.hcl new file mode 100644 index 0000000000..72d78f7cb2 --- /dev/null +++ b/test/fixtures/hclvalidate/valid/single-required-input/terragrunt.hcl @@ -0,0 +1,3 @@ +inputs = { + input = "value" +} diff --git a/test/fixtures/hclvalidate/valid/var-in-source/main.tf b/test/fixtures/hclvalidate/valid/var-in-source/main.tf new file mode 100644 index 0000000000..c5fd57132a --- /dev/null +++ b/test/fixtures/hclvalidate/valid/var-in-source/main.tf @@ -0,0 +1,8 @@ +locals { + variable_source = "github.com/foo/bar" +} + +module "module" { + source = local.variable_source + version = "0.0.0" +} diff --git a/test/fixtures/hclvalidate/valid/var-in-source/terragrunt.hcl b/test/fixtures/hclvalidate/valid/var-in-source/terragrunt.hcl new file mode 100644 index 0000000000..65e2cc3404 --- /dev/null +++ b/test/fixtures/hclvalidate/valid/var-in-source/terragrunt.hcl @@ -0,0 +1 @@ +// intentionally blank diff --git a/test/fixtures/hclvalidate/valid/var-in-version/main.tf b/test/fixtures/hclvalidate/valid/var-in-version/main.tf new file mode 100644 index 0000000000..0dc18b05e2 --- /dev/null +++ b/test/fixtures/hclvalidate/valid/var-in-version/main.tf @@ -0,0 +1,8 @@ +locals { + variable_version = "0.0.0" +} + +module "module" { + source = "github.com/foo/bar" + version = local.variable_version +} diff --git a/test/fixtures/hclvalidate/valid/var-in-version/terragrunt.hcl b/test/fixtures/hclvalidate/valid/var-in-version/terragrunt.hcl new file mode 100644 index 0000000000..65e2cc3404 --- /dev/null +++ b/test/fixtures/hclvalidate/valid/var-in-version/terragrunt.hcl @@ -0,0 +1 @@ +// intentionally blank diff --git a/test/integration_test.go b/test/integration_test.go index 430b1b41cc..ac73157291 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -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() diff --git a/tf/tf.go b/tf/tf.go index 5fd52323aa..ee59db8c89 100644 --- a/tf/tf.go +++ b/tf/tf.go @@ -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 ( @@ -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 }