diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml
index c5c5f66..8af3d6e 100644
--- a/.pre-commit-hooks.yaml
+++ b/.pre-commit-hooks.yaml
@@ -90,6 +90,14 @@
files: (\.hcl)$
exclude: \.terraform\/.*$
+- id: terragrunt_validate_inputs
+ name: Terragrunt validate inputs
+ description: Validates Terragrunt unused and undefined inputs.
+ entry: hooks/terragrunt_validate_inputs.sh
+ language: script
+ files: (\.hcl)$
+ exclude: \.terraform\/.*$
+
- id: tofu_tfsec
name: OpenTofu validate with tfsec (deprecated, use "tofu_trivy")
description:
diff --git a/README.md b/README.md
index 023ad0c..afb9387 100644
--- a/README.md
+++ b/README.md
@@ -60,7 +60,7 @@ If you are using `pre-commit-opentofu` already or want to support its developmen
* [`checkov`](https://github.com/bridgecrewio/checkov) required for `tofu_checkov` hook.
* [`terraform-docs`](https://github.com/terraform-docs/terraform-docs) required for `tofu_docs` hook.
-* [`terragrunt`](https://terragrunt.gruntwork.io/docs/getting-started/install/) required for `terragrunt_validate` hook.
+* [`terragrunt`](https://terragrunt.gruntwork.io/docs/getting-started/install/) required for `terragrunt_validate` and `terragrunt_validate_inputs` hooks.
* [`terrascan`](https://github.com/tenable/terrascan) required for `terrascan` hook.
* [`TFLint`](https://github.com/terraform-linters/tflint) required for `tofu_tflint` hook.
* [`TFSec`](https://github.com/liamg/tfsec) required for `tofu_tfsec` hook.
@@ -266,6 +266,32 @@ TAG=latest
docker run --rm --entrypoint cat tofuutils/pre-commit-opentofu:$TAG /usr/bin/tools_versions_info
```
+### Example: Terragrunt Input Validation
+
+Use `terragrunt_validate_inputs` to check that Terragrunt inputs line up with the module variables they are passed into:
+
+```yaml
+repos:
+- repo: https://github.com/tofuutils/pre-commit-opentofu
+ rev: # Get the latest from: https://github.com/tofuutils/pre-commit-opentofu/releases
+ hooks:
+ - id: terragrunt_fmt
+ - id: terragrunt_validate_inputs
+ args:
+ - --args=--terragrunt-strict-validate
+```
+
+> **Note**: This hook automatically uses `terragrunt validate-inputs` for older Terragrunt releases and `terragrunt hcl validate --inputs` for newer releases.
+>
+> If Terragrunt reports intermittent `.terragrunt-cache` download or `file exists` errors in your repository, run this hook serially in your consumer configuration:
+>
+> ```yaml
+> - id: terragrunt_validate_inputs
+> require_serial: true
+> args:
+> - --args=--terragrunt-strict-validate
+> ```
+
## Available Hooks
There are several [pre-commit](https://pre-commit.com/) hooks to keep OpenTofu configurations (both `*.tf` and `*.tfvars`) and Terragrunt configurations (`*.hcl`) in a good shape:
@@ -286,6 +312,7 @@ There are several [pre-commit](https://pre-commit.com/) hooks to keep OpenTofu c
| `tofu_validate` | Validates all Terraform configuration files. [Hook notes](#tofu_validate) | `jq`, only for `--retry-once-with-cleanup` flag |
| `terragrunt_fmt` | Reformat all [Terragrunt](https://github.com/gruntwork-io/terragrunt) configuration files (`*.hcl`) to a canonical format. | `terragrunt` |
| `terragrunt_validate` | Validates all [Terragrunt](https://github.com/gruntwork-io/terragrunt) configuration files (`*.hcl`) | `terragrunt` |
+| `terragrunt_validate_inputs` | Validates Terragrunt unused and undefined inputs. | `terragrunt` |
| `tofu_wrapper_module_for_each` | Generates OpenTofu wrappers with `for_each` in module. [Hook notes](#terraform_wrapper_module_for_each) | `hcledit` |
| `terrascan` | [terrascan](https://github.com/tenable/terrascan) Detect compliance and security violations. [Hook notes](#terrascan) | `terrascan` |
| `tfupdate` | [tfupdate](https://github.com/minamijoyo/tfupdate) Update version constraints of OpenTofu core, providers, and modules. [Hook notes](#tfupdate) | `tfupdate` |
diff --git a/hooks/_common.sh b/hooks/_common.sh
index 133e457..3f4bdc4 100644
--- a/hooks/_common.sh
+++ b/hooks/_common.sh
@@ -112,7 +112,7 @@ function common::parse_and_export_env_vars {
while true; do
# Check if at least 1 env var exists in `$arg`
# shellcheck disable=SC2016 # '${' should not be expanded
- if [[ "$arg" =~ .*'${'[A-Z_][A-Z0-9_]+?'}'.* ]]; then
+ if [[ "$arg" =~ .*'${'[A-Z_][A-Z0-9_]*'}'.* ]]; then
# Get `ENV_VAR` from `.*${ENV_VAR}.*`
local env_var_name=${arg#*$\{}
env_var_name=${env_var_name%%\}*}
diff --git a/hooks/terragrunt_validate_inputs.sh b/hooks/terragrunt_validate_inputs.sh
new file mode 100755
index 0000000..a7b0f78
--- /dev/null
+++ b/hooks/terragrunt_validate_inputs.sh
@@ -0,0 +1,198 @@
+#!/usr/bin/env bash
+set -eo pipefail
+
+# globals variables
+# shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines
+readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
+# shellcheck source=_common.sh
+. "$SCRIPT_DIR/_common.sh"
+
+function main {
+ common::initialize "$SCRIPT_DIR"
+ common::parse_cmdline "$@"
+ common::export_provided_env_vars "${ENV_VARS[@]}"
+ common::parse_and_export_env_vars
+ # JFYI: terragrunt validate color already suppressed via PRE_COMMIT_COLOR=never
+
+ if terragrunt_version_ge_0_78; then
+ normalize_validate_args_for_modern_terragrunt
+ readonly SUBCOMMAND=("hcl" "validate" "--inputs")
+ readonly RUN_ALL_SUBCOMMAND=("run" "--all" "hcl" "validate" "--inputs")
+
+ # shellcheck disable=SC2153 # False positive
+ common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}"
+ return
+ fi
+
+ run_legacy_validate_inputs
+}
+
+function normalize_validate_args_for_modern_terragrunt {
+ local arg_idx
+
+ for arg_idx in "${!ARGS[@]}"; do
+ case "${ARGS[$arg_idx]}" in
+ --terragrunt-strict-validate|--strict-validate)
+ ARGS[$arg_idx]="--strict"
+ ;;
+ esac
+ done
+}
+
+function terragrunt_version_ge_0_78 {
+ local version_raw
+ local version
+ local major
+ local minor
+
+ version_raw=$(terragrunt --version 2>/dev/null || true)
+ version=$(echo "$version_raw" | sed -E 's/.*v?([0-9]+)\.([0-9]+)\.([0-9]+).*/\1.\2.\3/')
+
+ if [[ ! $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ return 1
+ fi
+
+ IFS=. read -r major minor _ <<< "$version"
+
+ if ((major > 0)); then
+ return 0
+ fi
+
+ if ((minor >= 78)); then
+ return 0
+ fi
+
+ return 1
+}
+
+function run_legacy_validate_inputs {
+ local -a unit_dirs=()
+ local final_exit_code=0
+ local dir_path
+
+ while read -r dir_path; do
+ if [[ -n $dir_path ]]; then
+ unit_dirs+=("$dir_path")
+ fi
+ done < <(legacy_unit_dirs_from_files)
+
+ if [[ ${#unit_dirs[@]} -eq 0 ]]; then
+ return 0
+ fi
+
+ # preserve errexit status
+ shopt -qo errexit && ERREXIT_IS_SET=true
+ set +e
+
+ for dir_path in "${unit_dirs[@]}"; do
+ pushd "$dir_path" > /dev/null || continue
+ terragrunt validate-inputs "${ARGS[@]}"
+
+ local exit_code=$?
+ if [ $exit_code -ne 0 ]; then
+ final_exit_code=$exit_code
+ fi
+
+ popd > /dev/null
+ done
+
+ [[ $ERREXIT_IS_SET ]] && set -e
+ exit $final_exit_code
+}
+
+function legacy_unit_dirs_from_files {
+ local -a unit_files=()
+ local file_with_path
+ local file_dir
+ local file_name
+
+ if common::is_hook_run_on_whole_repo "$HOOK_ID" "${FILES[@]}"; then
+ find . -type f -name terragrunt.hcl \
+ -not -path '*/.terragrunt-cache/*' \
+ -not -path '*/.terraform/*' \
+ | sort -u | while read -r unit_file; do
+ dirname "$unit_file"
+ done
+ return
+ fi
+
+ for file_with_path in "${FILES[@]}"; do
+ file_dir=$(dirname "$file_with_path")
+ file_name=$(basename "$file_with_path")
+
+ if [[ $file_name == terragrunt.hcl ]]; then
+ unit_files+=("$file_with_path")
+ continue
+ fi
+
+ while read -r unit_file; do
+ if [[ -n $unit_file ]]; then
+ unit_files+=("$unit_file")
+ fi
+ done < <(find "$file_dir" -type f -name terragrunt.hcl \
+ -not -path '*/.terragrunt-cache/*' \
+ -not -path '*/.terraform/*' | sort -u)
+ done
+
+ if [[ ${#unit_files[@]} -eq 0 ]]; then
+ find . -type f -name terragrunt.hcl \
+ -not -path '*/.terragrunt-cache/*' \
+ -not -path '*/.terraform/*' \
+ | sort -u | while read -r unit_file; do
+ dirname "$unit_file"
+ done
+ return
+ fi
+
+ printf '%s\n' "${unit_files[@]}" | sort -u | while read -r unit_file; do
+ dirname "$unit_file"
+ done
+}
+
+#######################################################################
+# Unique part of `common::per_dir_hook`. The function is executed in loop
+# on each provided dir path. Run wrapped tool with specified arguments
+# Arguments:
+# dir_path (string) PATH to dir relative to git repo root.
+# Can be used in error logging
+# change_dir_in_unique_part (string/false) Modifier which creates
+# possibilities to use non-common chdir strategies.
+# Availability depends on hook.
+# args (array) arguments that configure wrapped tool behavior
+# Outputs:
+# If failed - print out hook checks status
+#######################################################################
+function per_dir_hook_unique_part {
+ # shellcheck disable=SC2034 # Unused var.
+ local -r dir_path="$1"
+ # shellcheck disable=SC2034 # Unused var.
+ local -r change_dir_in_unique_part="$2"
+ shift 2
+ local -a -r args=("$@")
+
+ # pass the arguments to hook
+ terragrunt "${SUBCOMMAND[@]}" "${args[@]}"
+
+ # return exit code to common::per_dir_hook
+ local exit_code=$?
+ return $exit_code
+}
+
+#######################################################################
+# Unique part of `common::per_dir_hook`. The function is executed one time
+# in the root git repo
+# Arguments:
+# args (array) arguments that configure wrapped tool behavior
+#######################################################################
+function run_hook_on_whole_repo {
+ local -a -r args=("$@")
+
+ # pass the arguments to hook
+ terragrunt "${RUN_ALL_SUBCOMMAND[@]}" "${args[@]}"
+
+ # return exit code to common::per_dir_hook
+ local exit_code=$?
+ return $exit_code
+}
+
+[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@"