diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index b9ab341a8..61065ff9c 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -178,3 +178,15 @@ - --args=terraform files: \.(tf|tofu)$ require_serial: true + +- id: terraform_provider_version_consistency + name: Terraform provider version consistency + description: >- + Checks that provider version constraints are consistent across all + versions.tf files in the repository. + entry: hooks/terraform_provider_version_consistency.sh + language: script + pass_filenames: false + files: versions\.tf$ + exclude: \.terraform/.*$ + require_serial: true diff --git a/README.md b/README.md index c4adbe194..ce11e2af8 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ If you want to support the development of `pre-commit-terraform` and [many other * [terraform\_tfsec (deprecated)](#terraform_tfsec-deprecated) * [terraform\_trivy](#terraform_trivy) * [terraform\_validate](#terraform_validate) + * [terraform\_provider\_version\_consistency](#terraform_provider_version_consistency) * [terraform\_wrapper\_module\_for\_each](#terraform_wrapper_module_for_each) * [terrascan](#terrascan) * [tfupdate](#tfupdate) @@ -337,6 +338,7 @@ There are several [pre-commit](https://pre-commit.com/) hooks to keep Terraform | `terraform_tfsec` | [TFSec][tfsec repo] static analysis of terraform templates to spot potential security issues. **DEPRECATED**, use `terraform_trivy`. [Hook notes](#terraform_tfsec-deprecated) | `tfsec` | | `terraform_trivy` | [Trivy][trivy repo] static analysis of terraform templates to spot potential security issues. [Hook notes](#terraform_trivy) | `trivy` | | `terraform_validate` | Validates all Terraform configuration files. [Hook notes](#terraform_validate) | `jq`, only for `--retry-once-with-cleanup` flag | +| `terraform_provider_version_consistency` | Checks that provider version constraints are consistent across all `versions.tf` files. [Hook notes](#terraform_provider_version_consistency) | - | | `terragrunt_fmt` | Reformat all [Terragrunt][terragrunt repo] configuration files (`*.hcl`) to a canonical format. | `terragrunt` | | `terragrunt_validate` | Validates all [Terragrunt][terragrunt repo] configuration files (`*.hcl`) | `terragrunt` | | `terragrunt_validate_inputs` | Validates [Terragrunt][terragrunt repo] unused and undefined inputs (`*.hcl`) | | @@ -1085,9 +1087,53 @@ To replicate functionality in `terraform_docs` hook: - repo: https://github.com/pre-commit/pre-commit-hooks ``` - > **Tip** + > **Tip** > The latter method will leave an "aliased-providers.tf.json" file in your repo. You will either want to automate a way to clean this up or add it to your `.gitignore` or both. +### terraform_provider_version_consistency + +`terraform_provider_version_consistency` checks that provider version constraints are consistent across all `versions.tf` files in the repository. This is useful for multi-module repositories where each module has its own `versions.tf` file and you want to ensure they all use the same provider versions. + +The hook: + +- Finds all `versions.tf` files (excluding `.terraform/` directories) +- Extracts provider version constraints (`version = "..."` lines) +- Fails if different version constraints are found across files +- Reports which files have inconsistent versions + +Example configuration: + +```yaml +- id: terraform_provider_version_consistency +``` + +Example output when versions are inconsistent: + +```text +Inconsistent provider versions found across 6 files: + +--- ./examples/complete/versions.tf + version = ">= 6.31" +--- ./versions.tf + version = "= 6.30" + +Found 2 different version constraints: + version = "= 6.30" + version = ">= 6.31" +``` + +You can customize the hook behavior using `--hook-config`: + +1. `--version-file-pattern=...` - Pattern for version files (default: `versions.tf`) +2. `--exclude-pattern=...` - Pattern to exclude from search (default: `.terraform/`) + +```yaml +- id: terraform_provider_version_consistency + args: + - --hook-config=--version-file-pattern=versions.tf + - --hook-config=--exclude-pattern=.terraform/ +``` + ### terraform_wrapper_module_for_each `terraform_wrapper_module_for_each` generates module wrappers for Terraform modules (useful for Terragrunt where `for_each` is not supported). When using this hook without arguments it will create wrappers for the root module and all modules available in "modules" directory. diff --git a/hooks/terraform_provider_version_consistency.sh b/hooks/terraform_provider_version_consistency.sh new file mode 100755 index 000000000..cf5ac0866 --- /dev/null +++ b/hooks/terraform_provider_version_consistency.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +set -eo pipefail + +# globals variables +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +readonly SCRIPT_DIR +# shellcheck source=_common.sh +. "$SCRIPT_DIR/_common.sh" + +function main { + common::initialize "$SCRIPT_DIR" + common::parse_cmdline "$@" + common::export_provided_env_vars "${ENV_VARS[@]}" + + # Default patterns + local version_file_pattern="versions.tf" + local exclude_pattern='.terraform/' + + # Parse hook-specific config + IFS=";" read -r -a configs <<< "${HOOK_CONFIG[*]}" + for c in "${configs[@]}"; do + IFS="=" read -r -a config <<< "$c" + key="${config[0]## }" + local value=${config[1]} + + case $key in + --version-file-pattern) + if [[ -n "$value" ]]; then + version_file_pattern="$value" + fi + ;; + --exclude-pattern) + if [[ -n "$value" ]]; then + exclude_pattern="$value" + fi + ;; + esac + done + + check_provider_version_consistency "$version_file_pattern" "$exclude_pattern" +} + +####################################################################### +# Check that provider version constraints are consistent across all +# versions.tf files in the repository +# Arguments: +# version_file_pattern (string) Pattern for version files (default: versions.tf) +# exclude_pattern (string) Pattern to exclude (default: .terraform/) +# Outputs: +# If inconsistent - print out which files have different versions +####################################################################### +function check_provider_version_consistency { + local -r version_file_pattern="$1" + local -r exclude_pattern="$2" + + # Find all version files, excluding specified pattern + local version_files + version_files=$(find . -name "$version_file_pattern" -type f ! -path "*${exclude_pattern}*" 2>/dev/null | sort) + + if [[ -z "$version_files" ]]; then + common::colorify "yellow" "No $version_file_pattern files found" + return 0 + fi + + local file_count + file_count=$(echo "$version_files" | wc -l | tr -d ' ') + + if [[ "$file_count" -eq 1 ]]; then + common::colorify "green" "Only one $version_file_pattern file found, skipping consistency check" + return 0 + fi + + # Extract all unique provider version constraint lines + local all_versions + # shellcheck disable=SC2086 # Word splitting is intentional + all_versions=$(grep -hE '^\s*version\s*=' $version_files 2>/dev/null | \ + sed 's/^[[:space:]]*//' | sort -u) + + # Handle case where no provider version constraints found + if [[ -z "$all_versions" ]]; then + common::colorify "yellow" "No provider version constraints found in $version_file_pattern files" + return 0 + fi + + local unique_count + unique_count=$(echo "$all_versions" | wc -l | tr -d ' ') + + if [[ "$unique_count" -eq 1 ]]; then + common::colorify "green" "All provider versions are consistent across $file_count files" + return 0 + fi + + # Versions are inconsistent - report details + common::colorify "red" "Inconsistent provider versions found across $file_count files:" + echo "" + + local file + # shellcheck disable=SC2086 # Word splitting is intentional + for file in $version_files; do + echo "--- $file" + grep -E '^\s*version\s*=' "$file" 2>/dev/null | sed 's/^/ /' + done + + echo "" + common::colorify "yellow" "Found $unique_count different version constraints:" + echo "$all_versions" | sed 's/^/ /' + + return 1 +} + +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@"