diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 1da68503e..7f1510d38 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -134,6 +134,9 @@ You can use [this PR](https://github.com/antonbabenko/pre-commit-terraform/pull/ * `docker build -t pre-commit --build-arg INSTALL_ALL=true .` * `docker build -t pre-commit --build-arg _VERSION=latest .` * `docker build -t pre-commit --build-arg _VERSION=<1.2.3> .` + + > [!NOTE] + > `tools/install/.sh` has a second call site besides the Dockerfile: if the new tool is distributed as a GitHub (or HashiCorp) release binary, `hooks/_common.sh::common::resolve_tool_path` invokes this same script at hook run-time to support [`--hook-config=--tool-version=`](../README.md#most-hooks-pin-a-specific-tool-version). Wire your new hook to it the same way the existing binary-wrapping hooks are (look at how `terraform_tflint.sh` or `terrascan.sh` call `common::resolve_tool_path`), unless the tool has a different distribution model (e.g. pip, like `checkov`) - in that case, skip this and leave a comment explaining why, as `terraform_checkov.sh` does. 2. Add Docker structure tests to [`.github/.container-structure-test-config.yaml`](.container-structure-test-config.yaml) 3. Add new hook to [`.pre-commit-hooks.yaml`](../.pre-commit-hooks.yaml) 4. Create hook file. Don't forget to make it executable via `chmod +x /path/to/hook/file`. diff --git a/README.md b/README.md index a1c58bc0b..97508d722 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ If you want to support the development of `pre-commit-terraform` and [many other * [All hooks: Set env vars inside hook at runtime](#all-hooks-set-env-vars-inside-hook-at-runtime) * [All hooks: Disable color output](#all-hooks-disable-color-output) * [All hooks: Log levels](#all-hooks-log-levels) + * [Most hooks: Pin a specific tool version](#most-hooks-pin-a-specific-tool-version) + * [Keeping pinned versions up-to-date using Renovate](#keeping-pinned-versions-up-to-date-using-renovate) * [Many hooks: Parallelism](#many-hooks-parallelism) * [checkov (deprecated) and terraform\_checkov](#checkov-deprecated-and-terraform_checkov) * [infracost\_breakdown](#infracost_breakdown) @@ -76,6 +78,7 @@ If you want to support the development of `pre-commit-terraform` and [many other * [About Docker image security](#about-docker-image-security) * [File Permissions](#file-permissions) * [Download Terraform modules from private GitHub repositories](#download-terraform-modules-from-private-github-repositories) + * [Mount tools cache directory](#mount-tools-cache-directory) * [GitHub Actions](#github-actions) * [Authors](#authors) * [License](#license) @@ -433,6 +436,89 @@ PCT_LOG=trace pre-commit run -a Less verbose log levels will be implemented in [#562](https://github.com/antonbabenko/pre-commit-terraform/issues/562). +### Most hooks: Pin a specific tool version + +> All hooks, which wrap a tool distributed as a downloadable release asset. Not supported for `checkov`/`terraform_checkov` (distributed via PyPi) and for deprecated `terraform_docs_replace` hook. + +1. You can pin a specific version of the wrapped tool per hook, independent of whatever is on your `$PATH` or baked into the Docker image. If that version isn't already cached locally, it's downloaded from the tool's GitHub releases on first use, then reused (without re-downloading) on every subsequent run. + + Config example: + + ```yaml + - id: terraform_tflint + args: + - --hook-config=--tool-version=0.50.0 + ``` + +2. The same `--tool-version` key also works for `terraform_validate`, `terraform_fmt` and `terraform_providers_lock`, which resolve their Terraform/OpenTofu binary through [`--tf-path`](#11-custom-terraform-binaries-and-opentofu-support) - by default it downloads/uses whichever of `terraform`/`opentofu` binary the rest of that precedence chain would otherwise have picked (`terraform`, unless it's missing from `$PATH` while `tofu` is present, in which case `opentofu`). To pick explicitly instead of relying on the auto-detection mechanism, set `--tf-path` to the literal value of `terraform`, `opentofu` or `tofu`: + + ```yaml + - id: terraform_validate + args: + - --hook-config=--tf-path=opentofu + - --hook-config=--tool-version=1.12.0 + ``` + + `--tf-path` and `--tool-version` either can be set on its own, or combined: + + | `--tf-path` | `--tool-version` | Behavior | + | ------------------------------------------ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | + | set to a literal path/binary name | unset | Use that local binary as-is (normal [`--tf-path`](#11-custom-terraform-binaries-and-opentofu-support) behavior); `--tool-version` is ignored | + | unset | set | Auto-detect `terraform`/`opentofu` as described above, then download/use that tool at the pinned version | + | set to `terraform`, `opentofu`, or `tofu` | set | Download/use the pinned version of whichever of the two was selected | + | set to anything else (e.g. an actual path) | set | Error - combined with `--tool-version`, `--tf-path` only accepts `terraform`, `opentofu`, or `tofu` | + | unset | unset | Falls back to the normal [`--tf-path`](#11-custom-terraform-binaries-and-opentofu-support) precedence chain (env vars, then `$PATH`) | + +> [!TIP] +> 3. Since this resolves to a version-specific cached binary rather than mutating `$PATH`, the same hook can be listed multiple times with different pinned versions, e.g. to test compatibility across tool versions in one run: +> +> ```yaml +> - id: terraform_tflint +> args: +> - --hook-config=--tool-version=0.50.0 +> - id: terraform_tflint +> args: +> - --hook-config=--tool-version=0.55.0 +> ``` + +4. By default, if a different version of the tool is already on `$PATH`, the pinned version still wins (with a warning message logged) based on `--hook-config=--tool-version-mode=strict`. Set it to `prefer-local` (as opposite to `strict`) to invert that: if the tool already resolves via `$PATH`, this local binary is used as-is and no download is attempted; the pinned version is only downloaded/used as a fallback when nothing is found locally. + + ```yaml + - id: terraform_tflint + args: + - --hook-config=--tool-version=0.50.0 + - --hook-config=--tool-version-mode=prefer-local + ``` + +5. By default, downloaded binaries are cached under `$XDG_CACHE_HOME/pre-commit-terraform/` (or `$HOME/.cache/pre-commit-terraform/` if `XDG_CACHE_HOME` is unset). Override this location with the `PCT_TOOL_CACHE_DIR` environment variable - see [Mount tools cache directory](#mount-tools-cache-directory) for the Docker case. + +6. If a `GITHUB_TOKEN` environment variable is set, it's inherited automatically to authenticate GitHub API requests made during version resolution, the same way it already is utilized for [building your own Docker image](#docker-usage). + +#### Keeping pinned versions up-to-date using Renovate + +Neither Renovate's built-in [`pre-commit` manager](https://docs.renovatebot.com/modules/manager/pre-commit/) (which only understands `repo:`/`rev:` and `additional_dependencies` for Go/Node/Python) nor its `dockerfileVersions` preset can "look" inside hook's `args:`, so the `--tool-version` pin needs its own [`customManagers`](https://docs.renovatebot.com/modules/manager/regex/) entry in your own `renovate.json5` config file, using the same `# renovate: datasource=... depName=...` annotation convention commonly used for Dockerfile `ARG *_VERSION` pins: + +```yaml +- id: terraform_tflint + args: + # renovate: datasource=github-releases depName=terraform-linters/tflint + - --hook-config=--tool-version=0.50.0 +``` + +```json5 +{ + customManagers: [ + { + customType: "regex", + managerFilePatterns: ["/\\.pre-commit-config\\.ya?ml$/"], + matchStrings: [ + "# renovate: datasource=(?\\S+) depName=(?\\S+)\\s+-\\s+--hook-config=--tool-version=(?\\S+)", + ], + }, + ], +} +``` + ### Many hooks: Parallelism > All, except deprecated hooks: `checkov`, `terraform_docs_replace` and hooks which can't be paralleled this way: `infracost_breakdown`, `terraform_wrapper_module_for_each`. @@ -1276,6 +1362,21 @@ Finally, you can execute `docker run` with an additional volume mount so that th docker run --rm -e "USERID=$(id -u):$(id -g)" -v ~/.netrc:/root/.netrc -v $(pwd):/lint -w /lint ghcr.io/antonbabenko/pre-commit-terraform:latest run -a ``` +### Mount tools cache directory + +A container's own filesystem is discarded after `docker run` exits, so a version downloaded via [`--tool-version`](#most-hooks-pin-a-specific-tool-version) would otherwise be re-downloaded on every single run. Mount the cache directory as a volume to persist it across runs, the same way you would for [`TF_PLUGIN_CACHE_DIR`](https://developer.hashicorp.com/terraform/cli/config/config-file#provider-plugin-cache): + +```bash +TAG=latest +docker run \ + -e "USERID=$(id -u):$(id -g)" \ + -v ~/.cache/pre-commit-terraform:/root/.cache/pre-commit-terraform \ + -v $(pwd):/lint -w /lint \ + ghcr.io/antonbabenko/pre-commit-terraform:$TAG run -a +``` + +If you set `PCT_TOOL_CACHE_DIR` to a custom location, mount that path instead (and pass the same env var to the container with `-e PCT_TOOL_CACHE_DIR=...`). + ## GitHub Actions You can use this hook in your GitHub Actions workflow together with [pre-commit](https://pre-commit.com). To easy up diff --git a/hooks/_common.sh b/hooks/_common.sh index e59a22b95..0fc0831a2 100644 --- a/hooks/_common.sh +++ b/hooks/_common.sh @@ -309,14 +309,16 @@ function common::get_cpu_num { # 3. Complete hook execution and return exit code # Arguments: # hook_id (string) hook ID, see `- id` for details in .pre-commit-hooks.yaml file +# tool_name (string) name of the wrapped tool, used to resolve its path # args_array_length (integer) Count of arguments in args array. # args (array) arguments that configure wrapped tool behavior # files (array) filenames to check ####################################################################### function common::per_dir_hook { local -r hook_id="$1" - local -i args_array_length=$2 - shift 2 + local -r tool_name="$2" + local -i args_array_length=$3 + shift 3 local -a args=() # Expand args to a true array. # Based on https://stackoverflow.com/a/10953834 @@ -328,13 +330,16 @@ function common::per_dir_hook { # despite there's only one positional ARG left local -a -r files=("$@") - local -r tf_path=$(common::get_tf_binary_path) + local -r tool_version=$(common::get_hook_config_value "--tool-version") + local tool_path + tool_path=$(common::resolve_tool_path "$tool_name" "$tool_version") || exit $? + readonly tool_path # check is (optional) function defined if [ "$(type -t run_hook_on_whole_repo)" == function ] && # check is hook run via `pre-commit run --all` common::is_hook_run_on_whole_repo "$hook_id" "${files[@]}"; then - run_hook_on_whole_repo "${args[@]}" + run_hook_on_whole_repo "$tool_path" "${args[@]}" exit 0 fi @@ -430,7 +435,7 @@ function common::per_dir_hook { pushd "$dir_path" > /dev/null fi - per_dir_hook_unique_part "$dir_path" "$change_dir_in_unique_part" "$parallelism_disabled" "$tf_path" "${args[@]}" + per_dir_hook_unique_part "$dir_path" "$change_dir_in_unique_part" "$parallelism_disabled" "$tool_path" "${args[@]}" } & pids+=("$!") @@ -492,9 +497,206 @@ function common::colorify { echo -e "${COLOR}${TEXT}${RESET}" >&2 } +####################################################################### +# Look up a single `--hook-config=--key=value` entry's value. +# Globals: +# HOOK_CONFIG (array) arguments that configure hook behavior +# Arguments: +# key (string) hook-config key to look up, including its leading `--` +# (e.g. "--tool-version") +# Outputs: +# Prints the value if the key is present in $HOOK_CONFIG, prints +# nothing otherwise +####################################################################### +function common::get_hook_config_value { + local -r key="$1" + local config value + + for config in "${HOOK_CONFIG[@]}"; do + if [[ $config == "$key"=* ]]; then + value=${config#*=} + value=${value%;} + break + fi + done + + echo "$value" +} + +####################################################################### +# Detect current OS/architecture using the same naming convention +# `tools/install/*.sh` expects (normally provided automatically by +# Docker buildx as TARGETOS/TARGETARCH build args; outside of a Docker +# build they don't exist and must be derived here instead). +# Globals (init and populate): +# TARGETOS (string) +# TARGETARCH (string) +####################################################################### +function common::detect_os_arch { + TARGETOS="$(uname -s | tr '[:upper:]' '[:lower:]')" + TARGETARCH="$(uname -m)" + + case "$TARGETARCH" in + x86_64) TARGETARCH="amd64" ;; + aarch64 | arm64) TARGETARCH="arm64" ;; + esac + + export TARGETOS TARGETARCH +} + +####################################################################### +# Resolve a specific version of a wrapped tool's binary, downloading +# and caching it on demand if it isn't already cached. +# +# Reuses the existing `tools/install/.sh` installer scripts +# instead of re-implementing per-tool download logic. +# Requires a downloadable release binary to resolve. +# +# Environment variables: +# PCT_TOOL_CACHE_DIR (string) if set, used as the complete cache +# root path as-is +# XDG_CACHE_HOME (string) if set (and PCT_TOOL_CACHE_DIR is not), +# "$XDG_CACHE_HOME/pre-commit-terraform" is used as the cache root +# GITHUB_TOKEN (string) forwarded automatically, since it's read +# directly by the invoked installer script +# Arguments: +# tool (string) tool name: +# - matching a `tools/install/.sh` file and its expected +# `${TOOL^^}_VERSION` environment variable name; +# - "tf" for Terraform/OpenTofu, resolved via `common::get_tf_binary_path` +# - empty for hooks with no resolvable binary (e.g. checkov) +# version (string) exact version requested (e.g. "1.7.5"), or empty +# if no `--tool-version` was requested +# Outputs: +# Prints the absolute path to the resolved binary, or the bare $tool +# name unchanged if no version was requested (empty string if $tool +# itself is also empty). If a download is attempted and fails - exit +# 1 with an error message. +####################################################################### +function common::resolve_tool_path { + local -r tool_name="$1" + local -r version="$2" + + # + # Check if configuration is valid + # + + # No resolvable tool name (e.g. checkov, which is pip-distributed); + # keeps "--tool-version" a documented no-op for it instead of erroring on an empty tool name. + [[ ! $tool_name ]] && return + + # "tf" is a placeholder, not a real tool. Delegate to + # `common::get_tf_binary_path`, which applies the extra precedence rules + # (--tf-path, PCT_TFPATH/TERRAGRUNT_TFPATH, terraform-vs-opentofu choice) + # then calls back here with the concrete name - which no longer matches + # "tf", so it falls through below instead of recursing. + if [[ $tool_name == "tf" ]]; then + common::get_tf_binary_path "$version" + return + fi + + if [[ ! $version ]]; then + # Check if the tool discoverable in the system's PATH + if ! command -v "$tool_name" > /dev/null; then + common::colorify "red" \ + "ERROR: '$tool_name' is required by '$HOOK_ID' pre-commit hook but it is not discoverable in the system's PATH.\n" \ + "Since '--hook-config=--tool-version=…' was not specified, no version resolution was attempted.\n\n" \ + "Please install '$tool_name' manually or specify in .pre-commit-config.yaml a version to download and cache via:\n" \ + "args:\n" \ + " - --hook-config=--tool-version=" + exit 1 + fi + + echo "$tool_name" + return + fi + + # + # Choose whether to prefer the local $PATH version of a tool over a requested version, if both exist. + # + local -r tool_version_mode=$(common::get_hook_config_value "--tool-version-mode") + + if command -v "$tool_name" &> /dev/null; then + if [[ $tool_version_mode == "prefer-local" ]]; then + common::colorify "green" \ + "NOTE: version '$version' was requested for '$tool_name', but '--tool-version-mode=prefer-local' " \ + "is set and '$tool_name' is already found on \$PATH - using that instead." + command -v "$tool_name" + return + fi + + common::colorify "green" \ + "NOTE: The requested '$tool_name' version '$version' will be downloaded/used instead of whatever is on \$PATH." + fi + + # + # Check if the requested version is already cached + # + + # opentofu.sh renames its binary from "opentofu" back to "tofu" after + # common::install_from_gh_release completes (see tools/install/opentofu.sh) + local resolved_bin_name="$tool_name" + [[ $tool_name == "opentofu" ]] && resolved_bin_name="tofu" + + local -r cache_root="${PCT_TOOL_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/pre-commit-terraform}" + local -r cache_dir="$cache_root/$tool_name/$version" + local -r cached_bin="$cache_dir/$resolved_bin_name" + + if [[ -x $cached_bin ]]; then + echo "$cached_bin" + return + fi + + # + # Download and cache the requested version + # + + local -r script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" + local -r installer_script="$script_dir/../tools/install/${tool_name}.sh" + + if [[ ! -f $installer_script ]]; then + common::colorify "red" "ERROR: pinning a version is not supported for '$tool_name' (no installer found at '$installer_script')." + exit 1 + fi + + common::colorify "green" "Downloading '$tool_name' version '$version'..." + + common::detect_os_arch + + local env_var_name="${tool_name//-/_}" + env_var_name="${env_var_name^^}_VERSION" + + mkdir -p "$cache_dir" + + # Redirect the installer's own stdout to stderr: this function's stdout is + # a contract (the resolved path, captured via "$(...)" by every caller), + # and installers like terraform.sh/tflint.sh call bare `unzip` (no `-q`), + # which prints "Archive: ... inflating: ..." to stdout by default - + # harmless noise in a Docker build log, but it would otherwise corrupt + # the path this function returns. + if ! ( + cd "$cache_dir" || exit 1 + export "$env_var_name=$version" + "$installer_script" 1>&2 + ); then + common::colorify "red" "ERROR: Failed to download '$tool_name' version '$version' via '$installer_script'." + exit 1 + fi + + if [[ ! -x $cached_bin ]]; then + common::colorify "red" "ERROR: '$tool_name' installer completed but expected binary was not found at '$cached_bin'." + exit 1 + fi + + echo "$cached_bin" +} + ####################################################################### # Get Terraform/OpenTofu binary path # Allows user to set the path to custom Terraform or OpenTofu binary +# Arguments: +# tool_version (string) value of a requested `--tool-version` +# hook-config, or empty if none was requested # Globals (init and populate): # HOOK_CONFIG (array) arguments that configure hook behavior # PCT_TFPATH (string) user defined env var with path to Terraform/OpenTofu binary @@ -503,21 +705,45 @@ function common::colorify { # If failed - exit 1 with error message about missing Terraform/OpenTofu binary ####################################################################### function common::get_tf_binary_path { - local hook_config_tf_path + local -r tool_version="$1" - for config in "${HOOK_CONFIG[@]}"; do - if [[ $config == --tf-path=* ]]; then - hook_config_tf_path=${config#*=} - hook_config_tf_path=${hook_config_tf_path%;} - break - fi - done + local -r hook_config_tf_path=$(common::get_hook_config_value "--tf-path") - # direct hook config, has the highest precedence - if [[ $hook_config_tf_path ]]; then + # direct hook config, has the highest precedence - but only when NOT + # combined with --tool-version. When it IS also set, --tf-path is + # reinterpreted below as an explicit terraform/opentofu selector + # rather than a literal binary path. + if [[ $hook_config_tf_path && ! $tool_version ]]; then echo "$hook_config_tf_path" return + # '--hook-config=--tool-version=X.Y.Z': download/cache a pinned + # Terraform/OpenTofu version on demand. + elif [[ $tool_version ]]; then + local tf_tool + case "$hook_config_tf_path" in + terraform) + tf_tool="terraform" + ;; + opentofu | tofu) + tf_tool="opentofu" + ;; + "") + # Terraform preferred; opentofu only if terraform isn't on $PATH but tofu is). + tf_tool="terraform" + ! command -v terraform &> /dev/null && command -v tofu &> /dev/null && tf_tool="opentofu" + ;; + *) + common::colorify "red" \ + "ERROR: '--tf-path=$hook_config_tf_path' combined with '--tool-version' is not a valid value.\n" \ + "'--tf-path=' must be either 'terraform', 'opentofu'/'tofu', or unset." + exit 1 + ;; + esac + + common::resolve_tool_path "$tf_tool" "$tool_version" + return + # environment variable elif [[ $PCT_TFPATH ]]; then echo "$PCT_TFPATH" @@ -539,7 +765,12 @@ function common::get_tf_binary_path { return else - common::colorify "red" "Neither Terraform nor OpenTofu binary could be found. Please either set the \"--tf-path\" hook configuration argument, or set the \"PCT_TFPATH\" environment variable, or set the \"TERRAGRUNT_TFPATH\" environment variable, or install Terraform or OpenTofu globally." + common::colorify "red" \ + 'Neither Terraform nor OpenTofu binary could be found. Please do one of the following:\n' \ + '- set the "--tf-path" hook configuration argument, along with "--tool-version" (to download and cache) or without it (to use already installed one)\n' \ + '- set the "PCT_TFPATH" environment variable\n' \ + '- set the "TERRAGRUNT_TFPATH" environment variable\n' \ + '- install Terraform or OpenTofu yourself and run "pre-commit" again' exit 1 fi } @@ -639,11 +870,15 @@ function common::export_provided_env_vars { } ####################################################################### -# Check if the installed Terragrunt version is >=0.78.0 or not +# Check if the given Terragrunt binary's version is >=0.78.0 or not # # This function helps to determine which terragrunt subcomand to use # based on Terragrunt version # +# Arguments: +# tool_path (string) resolved path to the terragrunt binary to check +# (the actually resolved/pinned binary, NOT whatever's on $PATH - +# those can differ once --tool-version is in play) # Returns: # - 0 if version >= 0.78.0 # - 1 if version < 0.78.0 @@ -651,10 +886,11 @@ function common::export_provided_env_vars { ####################################################################### # TODO: Drop after May 2027. Two years to upgrade is more than enough. function common::terragrunt_version_ge_0.78 { + local -r tool_path="$1" local terragrunt_version # Extract version number (e.g., "terragrunt version v0.80.4" -> "0.80") - terragrunt_version=$(terragrunt --version 2> /dev/null | grep -oE '[0-9]+\.[0-9]+') + terragrunt_version=$("$tool_path" --version 2> /dev/null | grep -oE '[0-9]+\.[0-9]+') # If we can't parse version, default to newer command [[ ! $terragrunt_version ]] && return 0 diff --git a/hooks/infracost_breakdown.sh b/hooks/infracost_breakdown.sh index 14d34b82f..8c89acc6f 100755 --- a/hooks/infracost_breakdown.sh +++ b/hooks/infracost_breakdown.sh @@ -12,8 +12,11 @@ function main { common::parse_cmdline "$@" common::export_provided_env_vars "${ENV_VARS[@]}" common::parse_and_export_env_vars + + local -r tool_name="infracost" + # shellcheck disable=SC2153 # False positive - infracost_breakdown_ "${HOOK_CONFIG[*]}" "${ARGS[*]}" + infracost_breakdown_ "$tool_name" "${HOOK_CONFIG[*]}" "${ARGS[*]}" } ####################################################################### @@ -22,6 +25,7 @@ function main { # Environment variables: # PRE_COMMIT_COLOR (string) If set to `never` - do not colorize output # Arguments: +# tool_name (string) name of the wrapped tool, used to resolve its path # hook_config (string with array) arguments that configure hook behavior # args (string with array) arguments that configure wrapped tool behavior # Outputs: @@ -29,9 +33,15 @@ function main { # diff, summary about infracost check (non-supported resources etc.) ####################################################################### function infracost_breakdown_ { - local -r hook_config="$1" + local -r tool_name="$1" + local -r hook_config="$2" local args - read -r -a args <<< "$2" + read -r -a args <<< "$3" + + local -r tool_version=$(common::get_hook_config_value "--tool-version") + local tool_path + tool_path=$(common::resolve_tool_path "$tool_name" "$tool_version") || exit $? + readonly tool_path # Get hook settings IFS=";" read -r -a checks <<< "$hook_config" @@ -41,7 +51,7 @@ function infracost_breakdown_ { fi local RESULTS - RESULTS="$(infracost breakdown "${args[@]}" --format json)" + RESULTS="$("$tool_path" breakdown "${args[@]}" --format json)" local API_VERSION API_VERSION="$(jq -r .version <<< "$RESULTS")" diff --git a/hooks/terraform_checkov.sh b/hooks/terraform_checkov.sh index 39a93ef5f..368d055c7 100755 --- a/hooks/terraform_checkov.sh +++ b/hooks/terraform_checkov.sh @@ -18,8 +18,11 @@ function main { export ANSI_COLORS_DISABLED=true fi + # checkov is distributed via PyPi, not as GitHub release asset, so it has no + # resolved tool_path (see per_dir_hook_unique_part below) - pass an + # empty placeholder to satisfy common::per_dir_hook's signature. # shellcheck disable=SC2153 # ARGS is set in common::parse_cmdline - common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" + common::per_dir_hook "$HOOK_ID" "" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" } ####################################################################### @@ -33,7 +36,7 @@ function main { # Availability depends on hook. # parallelism_disabled (bool) if true - skip lock mechanism # args (array) arguments that configure wrapped tool behavior -# tf_path (string) PATH to Terraform/OpenTofu binary +# tool_path (string) unused - checkov has no resolved tool_path # Outputs: # If failed - print out hook checks status ####################################################################### @@ -45,7 +48,7 @@ function per_dir_hook_unique_part { # shellcheck disable=SC2034 # Unused var. local -r parallelism_disabled="$3" # shellcheck disable=SC2034 # Unused var. - local -r tf_path="$4" + local -r tool_path="$4" shift 4 local -a -r args=("$@") @@ -60,9 +63,13 @@ function per_dir_hook_unique_part { # Unique part of `common::per_dir_hook`. The function is executed one time # in the root git repo # Arguments: +# tool_path (string) unused - checkov has no resolved tool_path # args (array) arguments that configure wrapped tool behavior ####################################################################### function run_hook_on_whole_repo { + # shellcheck disable=SC2034 # Unused var. + local -r tool_path="$1" + shift local -a -r args=("$@") # pass the arguments to hook diff --git a/hooks/terraform_docs.sh b/hooks/terraform_docs.sh index 7644b5e4c..ff80dccae 100755 --- a/hooks/terraform_docs.sh +++ b/hooks/terraform_docs.sh @@ -24,8 +24,11 @@ function main { for i in "${!ARGS[@]}"; do ARGS[i]=${ARGS[i]/--config=/--config=$(pwd)\/} done + + local -r tool_name="terraform-docs" + # shellcheck disable=SC2153 # False positive - terraform_docs "${HOOK_CONFIG[*]}" "${ARGS[*]}" "${FILES[@]}" + terraform_docs "$tool_name" "${HOOK_CONFIG[*]}" "${ARGS[*]}" "${FILES[@]}" } ####################################################################### @@ -52,20 +55,22 @@ function replace_old_markers { # (depending on provided hook_config) terraform documentation in # Markdown # Arguments: +# tool_name (string) name of the wrapped tool, used to resolve its path # hook_config (string with array) arguments that configure hook behavior # args (string with array) arguments that configure wrapped tool behavior # files (array) filenames to check ####################################################################### function terraform_docs { - local -r hook_config="$1" - local args="$2" - shift 2 + local -r tool_name="$1" + local -r hook_config="$2" + local args="$3" + shift 3 local -a -r files=("$@") - if [[ ! $(command -v terraform-docs) ]]; then - echo "ERROR: terraform-docs is required by terraform_docs pre-commit hook but is not installed or in the system's PATH." - exit 1 - fi + local -r tool_version=$(common::get_hook_config_value "--tool-version") + local tool_path + tool_path=$(common::resolve_tool_path "$tool_name" "$tool_version") || exit $? + readonly tool_path local -a paths @@ -244,7 +249,7 @@ function terraform_docs { # shellcheck disable=SC2206 # Need to pass $tf_docs_formatter and $args as separate arguments, not as single string local tfdocs_cmd=( - terraform-docs + "$tool_path" --output-mode="$output_mode" --output-file="$output_file" $tf_docs_formatter diff --git a/hooks/terraform_fmt.sh b/hooks/terraform_fmt.sh index e8f974fd7..5bf4bd0fb 100755 --- a/hooks/terraform_fmt.sh +++ b/hooks/terraform_fmt.sh @@ -18,8 +18,10 @@ function main { ARGS+=("-no-color") fi + local -r tool_name="tf" # Will be resolved into real tool inside 'common::resolve_tool_path' + # shellcheck disable=SC2153 # False positive - common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" + common::per_dir_hook "$HOOK_ID" "$tool_name" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" } ####################################################################### @@ -33,7 +35,7 @@ function main { # Availability depends on hook. # parallelism_disabled (bool) if true - skip lock mechanism # args (array) arguments that configure wrapped tool behavior -# tf_path (string) PATH to Terraform/OpenTofu binary +# tool_path (string) PATH to Terraform/OpenTofu binary # Outputs: # If failed - print out hook checks status ####################################################################### @@ -44,12 +46,12 @@ function per_dir_hook_unique_part { local -r change_dir_in_unique_part="$2" # shellcheck disable=SC2034 # Unused var. local -r parallelism_disabled="$3" - local -r tf_path="$4" + local -r tool_path="$4" shift 4 local -a -r args=("$@") # pass the arguments to hook - "$tf_path" fmt "${args[@]}" + "$tool_path" fmt "${args[@]}" # return exit code to common::per_dir_hook local exit_code=$? diff --git a/hooks/terraform_providers_lock.sh b/hooks/terraform_providers_lock.sh index 32283cd33..af5879b8b 100755 --- a/hooks/terraform_providers_lock.sh +++ b/hooks/terraform_providers_lock.sh @@ -15,8 +15,10 @@ function main { common::parse_and_export_env_vars # JFYI: suppress color for `terraform providers lock` is N/A` + local -r tool_name="tf" # Will be resolved into real tool inside 'common::resolve_tool_path' + # shellcheck disable=SC2153 # False positive - common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" + common::per_dir_hook "$HOOK_ID" "$tool_name" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" } ####################################################################### @@ -87,7 +89,7 @@ function lockfile_contains_all_needed_sha { # Availability depends on hook. # parallelism_disabled (bool) if true - skip lock mechanism # args (array) arguments that configure wrapped tool behavior -# tf_path (string) PATH to Terraform/OpenTofu binary +# tool_path (string) PATH to Terraform/OpenTofu binary # Outputs: # If failed - print out hook checks status ####################################################################### @@ -96,7 +98,7 @@ function per_dir_hook_unique_part { # shellcheck disable=SC2034 # Unused var. local -r change_dir_in_unique_part="$2" local -r parallelism_disabled="$3" - local -r tf_path="$4" + local -r tool_path="$4" shift 4 local -a -r args=("$@") @@ -161,7 +163,7 @@ Please update your configuration." if [ ! "$mode" ]; then common::colorify "yellow" "DEPRECATION NOTICE: We introduced '--mode' flag for this hook. Check migration instructions at https://github.com/antonbabenko/pre-commit-terraform#terraform_providers_lock" - common::terraform_init "$tf_path providers lock" "$dir_path" "$parallelism_disabled" "$tf_path" || { + common::terraform_init "$tool_path providers lock" "$dir_path" "$parallelism_disabled" "$tool_path" || { exit_code=$? return $exit_code } @@ -192,7 +194,7 @@ All required platforms: ${platforms_names[*]}" #? Don't require `tf init` for providers, but required `tf init` for modules #? Mitigated by `function match_validate_errors` from terraform_validate hook # pass the arguments to hook - "$tf_path" providers lock "${args[@]}" + "$tool_path" providers lock "${args[@]}" exit_code=$? if [[ $exit_code -ne 0 ]]; then diff --git a/hooks/terraform_tflint.sh b/hooks/terraform_tflint.sh index 1c2439f0f..9701dfb2b 100755 --- a/hooks/terraform_tflint.sh +++ b/hooks/terraform_tflint.sh @@ -16,11 +16,20 @@ function main { # JFYI: tflint color already suppressed via PRE_COMMIT_COLOR=never + local -r tool_name="tflint" + # Needed early for the `tflint --init` pre-flight check below, which + # runs once, before (and separately from) common::per_dir_hook's own + # resolution for the actual per-dir tflint runs. + local -r tool_version=$(common::get_hook_config_value "--tool-version") + local tool_path + tool_path=$(common::resolve_tool_path "$tool_name" "$tool_version") || exit $? + readonly tool_path + # Run `tflint --init` for check that plugins installed. # It should run once on whole repo. { # shellcheck disable=SC2153 # ARGS is set in common::parse_cmdline - TFLINT_INIT=$(tflint --init "${ARGS[@]}" 2>&1) 2> /dev/null && + TFLINT_INIT=$("$tool_path" --init "${ARGS[@]}" 2>&1) 2> /dev/null && common::colorify "green" "Command 'tflint --init' successfully done:" && echo -e "${TFLINT_INIT}\n\n\n" } || { @@ -30,7 +39,7 @@ function main { return ${exit_code} } - common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" + common::per_dir_hook "$HOOK_ID" "$tool_name" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" } ####################################################################### @@ -44,7 +53,7 @@ function main { # Availability depends on hook. # parallelism_disabled (bool) if true - skip lock mechanism # args (array) arguments that configure wrapped tool behavior -# tf_path (string) PATH to Terraform/OpenTofu binary +# tool_path (string) resolved path to the wrapped tool's binary # Outputs: # If failed - print out hook checks status ####################################################################### @@ -53,8 +62,7 @@ function per_dir_hook_unique_part { local -r change_dir_in_unique_part="$2" # shellcheck disable=SC2034 # Unused var. local -r parallelism_disabled="$3" - # shellcheck disable=SC2034 # Unused var. - local -r tf_path="$4" + local -r tool_path="$4" shift 4 local -a -r args=("$@") @@ -63,7 +71,7 @@ function per_dir_hook_unique_part { fi # shellcheck disable=SC2086 # we need to remove the arg if its unset - TFLINT_OUTPUT=$(tflint ${dir_args:-} "${args[@]}" 2>&1) + TFLINT_OUTPUT=$("$tool_path" ${dir_args:-} "${args[@]}" 2>&1) local exit_code=$? if [ $exit_code -ne 0 ]; then diff --git a/hooks/terraform_tfsec.sh b/hooks/terraform_tfsec.sh index aa1f3fc8b..c1a9d992a 100755 --- a/hooks/terraform_tfsec.sh +++ b/hooks/terraform_tfsec.sh @@ -21,7 +21,9 @@ function main { common::colorify "yellow" "tfsec tool was deprecated, and replaced by trivy. You can check trivy hook here:" common::colorify "yellow" "https://github.com/antonbabenko/pre-commit-terraform/tree/master#terraform_trivy" - common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" + local -r tool_name="tfsec" + + common::per_dir_hook "$HOOK_ID" "$tool_name" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" } ####################################################################### @@ -35,7 +37,7 @@ function main { # Availability depends on hook. # parallelism_disabled (bool) if true - skip lock mechanism # args (array) arguments that configure wrapped tool behavior -# tf_path (string) PATH to Terraform/OpenTofu binary +# tool_path (string) resolved path to the wrapped tool's binary # Outputs: # If failed - print out hook checks status ####################################################################### @@ -46,13 +48,12 @@ function per_dir_hook_unique_part { local -r change_dir_in_unique_part="$2" # shellcheck disable=SC2034 # Unused var. local -r parallelism_disabled="$3" - # shellcheck disable=SC2034 # Unused var. - local -r tf_path="$4" + local -r tool_path="$4" shift 4 local -a -r args=("$@") # pass the arguments to hook - tfsec "${args[@]}" + "$tool_path" "${args[@]}" # return exit code to common::per_dir_hook local exit_code=$? @@ -63,13 +64,15 @@ function per_dir_hook_unique_part { # Unique part of `common::per_dir_hook`. The function is executed one time # in the root git repo # Arguments: +# tool_path (string) resolved path to the wrapped tool's binary # args (array) arguments that configure wrapped tool behavior ####################################################################### function run_hook_on_whole_repo { + local -r tool_path="$1" + shift local -a -r args=("$@") - # pass the arguments to hook - tfsec "$(pwd)" "${args[@]}" + "$tool_path" "$(pwd)" "${args[@]}" # return exit code to common::per_dir_hook local exit_code=$? diff --git a/hooks/terraform_trivy.sh b/hooks/terraform_trivy.sh index 86f56f013..a2961e7c4 100755 --- a/hooks/terraform_trivy.sh +++ b/hooks/terraform_trivy.sh @@ -13,8 +13,10 @@ function main { common::export_provided_env_vars "${ENV_VARS[@]}" common::parse_and_export_env_vars + local -r tool_name="trivy" + # shellcheck disable=SC2153 # ARGS is set in common::parse_cmdline - common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" + common::per_dir_hook "$HOOK_ID" "$tool_name" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" } ####################################################################### @@ -28,7 +30,7 @@ function main { # Availability depends on hook. # parallelism_disabled (bool) if true - skip lock mechanism # args (array) arguments that configure wrapped tool behavior -# tf_path (string) PATH to Terraform/OpenTofu binary +# tool_path (string) resolved path to the wrapped tool's binary # Outputs: # If failed - print out hook checks status ####################################################################### @@ -39,13 +41,12 @@ function per_dir_hook_unique_part { local -r change_dir_in_unique_part="$2" # shellcheck disable=SC2034 # Unused var. local -r parallelism_disabled="$3" - # shellcheck disable=SC2034 # Unused var. - local -r tf_path="$4" + local -r tool_path="$4" shift 4 local -a -r args=("$@") # pass the arguments to hook - trivy conf "$(pwd)" --exit-code=1 "${args[@]}" + "$tool_path" conf "$(pwd)" --exit-code=1 "${args[@]}" # return exit code to common::per_dir_hook local exit_code=$? @@ -56,13 +57,15 @@ function per_dir_hook_unique_part { # Unique part of `common::per_dir_hook`. The function is executed one time # in the root git repo # Arguments: +# tool_path (string) resolved path to the wrapped tool's binary # args (array) arguments that configure wrapped tool behavior ####################################################################### function run_hook_on_whole_repo { + local -r tool_path="$1" + shift local -a -r args=("$@") - # pass the arguments to hook - trivy conf "$(pwd)" --exit-code=1 "${args[@]}" + "$tool_path" conf "$(pwd)" --exit-code=1 "${args[@]}" # return exit code to common::per_dir_hook local exit_code=$? diff --git a/hooks/terraform_validate.sh b/hooks/terraform_validate.sh index 21b635b84..8752a4cf9 100755 --- a/hooks/terraform_validate.sh +++ b/hooks/terraform_validate.sh @@ -21,8 +21,11 @@ function main { if [ "$PRE_COMMIT_COLOR" = "never" ]; then ARGS+=("-no-color") fi + + local -r tool_name="tf" # Will be resolved into real tool inside 'common::resolve_tool_path' + # shellcheck disable=SC2153 # False positive - common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" + common::per_dir_hook "$HOOK_ID" "$tool_name" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" } ####################################################################### @@ -82,7 +85,7 @@ function match_validate_errors { # Availability depends on hook. # parallelism_disabled (bool) if true - skip lock mechanism # args (array) arguments that configure wrapped tool behavior -# tf_path (string) PATH to Terraform/OpenTofu binary +# tool_path (string) PATH to Terraform/OpenTofu binary # Outputs: # If failed - print out hook checks status ####################################################################### @@ -91,7 +94,7 @@ function per_dir_hook_unique_part { # shellcheck disable=SC2034 # Unused var. local -r change_dir_in_unique_part="$2" local -r parallelism_disabled="$3" - local -r tf_path="$4" + local -r tool_path="$4" shift 4 local -a -r args=("$@") @@ -123,25 +126,25 @@ function per_dir_hook_unique_part { # First try `terraform validate` with the hope that all deps are # pre-installed. That is needed for cases when `.terraform/modules` # or `.terraform/providers` missed AND that is expected. - "$tf_path" validate "${args[@]}" &> /dev/null && { + "$tool_path" validate "${args[@]}" &> /dev/null && { exit_code=$? return $exit_code } # In case `terraform validate` failed to execute # - check is simple `terraform init` will help - common::terraform_init "$tf_path validate" "$dir_path" "$parallelism_disabled" "$tf_path" || { + common::terraform_init "$tool_path validate" "$dir_path" "$parallelism_disabled" "$tool_path" || { exit_code=$? return $exit_code } if [ "$retry_once_with_cleanup" != "true" ]; then # terraform validate only - validate_output=$("$tf_path" validate "${args[@]}" 2>&1) + validate_output=$("$tool_path" validate "${args[@]}" 2>&1) exit_code=$? else # terraform validate, plus capture possible errors - validate_output=$("$tf_path" validate -json "${args[@]}" 2>&1) + validate_output=$("$tool_path" validate -json "${args[@]}" 2>&1) exit_code=$? # Match specific validation errors @@ -159,12 +162,12 @@ function per_dir_hook_unique_part { common::colorify "yellow" "Re-validating: $dir_path" - common::terraform_init "$tf_path validate" "$dir_path" "$parallelism_disabled" "$tf_path" || { + common::terraform_init "$tool_path validate" "$dir_path" "$parallelism_disabled" "$tool_path" || { exit_code=$? return $exit_code } - validate_output=$("$tf_path" validate "${args[@]}" 2>&1) + validate_output=$("$tool_path" validate "${args[@]}" 2>&1) exit_code=$? fi fi diff --git a/hooks/terragrunt_fmt.sh b/hooks/terragrunt_fmt.sh index 2d6697ae3..1b55c5fd7 100755 --- a/hooks/terragrunt_fmt.sh +++ b/hooks/terragrunt_fmt.sh @@ -14,14 +14,23 @@ function main { common::parse_and_export_env_vars # JFYI: `terragrunt hcl format` color already suppressed via PRE_COMMIT_COLOR=never - if common::terragrunt_version_ge_0.78; then + local -r tool_name="terragrunt" + # Needed early to pick the correct CLI syntax below against the actually + # resolved/pinned terragrunt binary, not whatever's on $PATH. + # common::per_dir_hook resolves it again for the per-dir runs. + local -r tool_version=$(common::get_hook_config_value "--tool-version") + local tool_path + tool_path=$(common::resolve_tool_path "$tool_name" "$tool_version") || exit $? + readonly tool_path + + if common::terragrunt_version_ge_0.78 "$tool_path"; then local -ra SUBCOMMAND=(hcl format) else local -ra SUBCOMMAND=(hclfmt) fi # shellcheck disable=SC2153 # False positive - common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" + common::per_dir_hook "$HOOK_ID" "$tool_name" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" } ####################################################################### @@ -35,7 +44,7 @@ function main { # Availability depends on hook. # parallelism_disabled (bool) if true - skip lock mechanism # args (array) arguments that configure wrapped tool behavior -# tf_path (string) PATH to Terraform/OpenTofu binary +# tool_path (string) resolved path to the wrapped tool's binary # Outputs: # If failed - print out hook checks status ####################################################################### @@ -46,13 +55,12 @@ function per_dir_hook_unique_part { local -r change_dir_in_unique_part="$2" # shellcheck disable=SC2034 # Unused var. local -r parallelism_disabled="$3" - # shellcheck disable=SC2034 # Unused var. - local -r tf_path="$4" + local -r tool_path="$4" shift 4 local -a -r args=("$@") # pass the arguments to hook - terragrunt "${SUBCOMMAND[@]}" "${args[@]}" + "$tool_path" "${SUBCOMMAND[@]}" "${args[@]}" # return exit code to common::per_dir_hook local exit_code=$? @@ -63,13 +71,15 @@ function per_dir_hook_unique_part { # Unique part of `common::per_dir_hook`. The function is executed one time # in the root git repo # Arguments: +# tool_path (string) resolved path to the wrapped tool's binary # args (array) arguments that configure wrapped tool behavior ####################################################################### function run_hook_on_whole_repo { + local -r tool_path="$1" + shift local -a -r args=("$@") - # pass the arguments to hook - terragrunt "${SUBCOMMAND[@]}" "$(pwd)" "${args[@]}" + "$tool_path" "${SUBCOMMAND[@]}" "$(pwd)" "${args[@]}" # return exit code to common::per_dir_hook local exit_code=$? diff --git a/hooks/terragrunt_providers_lock.sh b/hooks/terragrunt_providers_lock.sh index cf6fb13ea..e7297778e 100755 --- a/hooks/terragrunt_providers_lock.sh +++ b/hooks/terragrunt_providers_lock.sh @@ -14,7 +14,16 @@ function main { common::parse_and_export_env_vars # JFYI: terragrunt providers lock color already suppressed via PRE_COMMIT_COLOR=never - if common::terragrunt_version_ge_0.78; then + local -r tool_name="terragrunt" + # Needed early to pick the correct CLI syntax below against the actually + # resolved/pinned terragrunt binary, not whatever's on $PATH. + # common::per_dir_hook resolves it again for the per-dir runs. + local -r tool_version=$(common::get_hook_config_value "--tool-version") + local tool_path + tool_path=$(common::resolve_tool_path "$tool_name" "$tool_version") || exit $? + readonly tool_path + + if common::terragrunt_version_ge_0.78 "$tool_path"; then local -ra SUBCOMMAND=(run -- providers lock) local -ra RUN_ALL_SUBCOMMAND=(run --all -- providers lock) else @@ -23,7 +32,7 @@ function main { fi # shellcheck disable=SC2153 # False positive - common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" + common::per_dir_hook "$HOOK_ID" "$tool_name" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" } ####################################################################### @@ -37,7 +46,7 @@ function main { # Availability depends on hook. # parallelism_disabled (bool) if true - skip lock mechanism # args (array) arguments that configure wrapped tool behavior -# tf_path (string) PATH to Terraform/OpenTofu binary +# tool_path (string) resolved path to the wrapped tool's binary # Outputs: # If failed - print out hook checks status ####################################################################### @@ -48,13 +57,12 @@ function per_dir_hook_unique_part { local -r change_dir_in_unique_part="$2" # shellcheck disable=SC2034 # Unused var. local -r parallelism_disabled="$3" - # shellcheck disable=SC2034 # Unused var. - local -r tf_path="$4" + local -r tool_path="$4" shift 4 local -a -r args=("$@") # pass the arguments to hook - terragrunt "${SUBCOMMAND[@]}" "${args[@]}" + "$tool_path" "${SUBCOMMAND[@]}" "${args[@]}" # return exit code to common::per_dir_hook local exit_code=$? @@ -65,13 +73,15 @@ function per_dir_hook_unique_part { # Unique part of `common::per_dir_hook`. The function is executed one time # in the root git repo # Arguments: +# tool_path (string) resolved path to the wrapped tool's binary # args (array) arguments that configure wrapped tool behavior ####################################################################### function run_hook_on_whole_repo { + local -r tool_path="$1" + shift local -a -r args=("$@") - # pass the arguments to hook - terragrunt "${RUN_ALL_SUBCOMMAND[@]}" "${args[@]}" + "$tool_path" "${RUN_ALL_SUBCOMMAND[@]}" "${args[@]}" # return exit code to common::per_dir_hook local exit_code=$? diff --git a/hooks/terragrunt_validate.sh b/hooks/terragrunt_validate.sh index a3f7e1d0a..b95bb8492 100755 --- a/hooks/terragrunt_validate.sh +++ b/hooks/terragrunt_validate.sh @@ -14,7 +14,16 @@ function main { common::parse_and_export_env_vars # JFYI: terragrunt validate color already suppressed via PRE_COMMIT_COLOR=never - if common::terragrunt_version_ge_0.78; then + local -r tool_name="terragrunt" + # Needed early to pick the correct CLI syntax below against the actually + # resolved/pinned terragrunt binary, not whatever's on $PATH. + # common::per_dir_hook resolves it again for the per-dir runs. + local -r tool_version=$(common::get_hook_config_value "--tool-version") + local tool_path + tool_path=$(common::resolve_tool_path "$tool_name" "$tool_version") || exit $? + readonly tool_path + + if common::terragrunt_version_ge_0.78 "$tool_path"; then local -ra SUBCOMMAND=(run -- validate) local -ra RUN_ALL_SUBCOMMAND=(run --all -- validate) else @@ -23,7 +32,7 @@ function main { fi # shellcheck disable=SC2153 # False positive - common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" + common::per_dir_hook "$HOOK_ID" "$tool_name" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" } ####################################################################### @@ -37,7 +46,7 @@ function main { # Availability depends on hook. # parallelism_disabled (bool) if true - skip lock mechanism # args (array) arguments that configure wrapped tool behavior -# tf_path (string) PATH to Terraform/OpenTofu binary +# tool_path (string) resolved path to the wrapped tool's binary # Outputs: # If failed - print out hook checks status ####################################################################### @@ -48,13 +57,12 @@ function per_dir_hook_unique_part { local -r change_dir_in_unique_part="$2" # shellcheck disable=SC2034 # Unused var. local -r parallelism_disabled="$3" - # shellcheck disable=SC2034 # Unused var. - local -r tf_path="$4" + local -r tool_path="$4" shift 4 local -a -r args=("$@") # pass the arguments to hook - terragrunt "${SUBCOMMAND[@]}" "${args[@]}" + "$tool_path" "${SUBCOMMAND[@]}" "${args[@]}" # return exit code to common::per_dir_hook local exit_code=$? @@ -65,13 +73,15 @@ function per_dir_hook_unique_part { # Unique part of `common::per_dir_hook`. The function is executed one time # in the root git repo # Arguments: +# tool_path (string) resolved path to the wrapped tool's binary # args (array) arguments that configure wrapped tool behavior ####################################################################### function run_hook_on_whole_repo { + local -r tool_path="$1" + shift local -a -r args=("$@") - # pass the arguments to hook - terragrunt "${RUN_ALL_SUBCOMMAND[@]}" "${args[@]}" + "$tool_path" "${RUN_ALL_SUBCOMMAND[@]}" "${args[@]}" # return exit code to common::per_dir_hook local exit_code=$? diff --git a/hooks/terragrunt_validate_inputs.sh b/hooks/terragrunt_validate_inputs.sh index 39e8e484e..9fda19d05 100755 --- a/hooks/terragrunt_validate_inputs.sh +++ b/hooks/terragrunt_validate_inputs.sh @@ -14,7 +14,16 @@ function main { common::parse_and_export_env_vars # JFYI: terragrunt validate color already suppressed via PRE_COMMIT_COLOR=never - if common::terragrunt_version_ge_0.78; then + local -r tool_name="terragrunt" + # Needed early to pick the correct CLI syntax below against the actually + # resolved/pinned terragrunt binary, not whatever's on $PATH. + # common::per_dir_hook resolves it again for the per-dir runs. + local -r tool_version=$(common::get_hook_config_value "--tool-version") + local tool_path + tool_path=$(common::resolve_tool_path "$tool_name" "$tool_version") || exit $? + readonly tool_path + + if common::terragrunt_version_ge_0.78 "$tool_path"; then local -ra SUBCOMMAND=(hcl validate --inputs) local -ra RUN_ALL_SUBCOMMAND=(run --all hcl validate --inputs) else @@ -23,7 +32,7 @@ function main { fi # shellcheck disable=SC2153 # False positive - common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" + common::per_dir_hook "$HOOK_ID" "$tool_name" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" } ####################################################################### @@ -37,7 +46,7 @@ function main { # Availability depends on hook. # parallelism_disabled (bool) if true - skip lock mechanism # args (array) arguments that configure wrapped tool behavior -# tf_path (string) PATH to Terraform/OpenTofu binary +# tool_path (string) resolved path to the wrapped tool's binary # Outputs: # If failed - print out hook checks status ####################################################################### @@ -48,13 +57,12 @@ function per_dir_hook_unique_part { local -r change_dir_in_unique_part="$2" # shellcheck disable=SC2034 # Unused var. local -r parallelism_disabled="$3" - # shellcheck disable=SC2034 # Unused var. - local -r tf_path="$4" + local -r tool_path="$4" shift 4 local -a -r args=("$@") # pass the arguments to hook - terragrunt "${SUBCOMMAND[@]}" "${args[@]}" + "$tool_path" "${SUBCOMMAND[@]}" "${args[@]}" # return exit code to common::per_dir_hook local exit_code=$? @@ -65,13 +73,15 @@ function per_dir_hook_unique_part { # Unique part of `common::per_dir_hook`. The function is executed one time # in the root git repo # Arguments: +# tool_path (string) resolved path to the wrapped tool's binary # args (array) arguments that configure wrapped tool behavior ####################################################################### function run_hook_on_whole_repo { + local -r tool_path="$1" + shift local -a -r args=("$@") - # pass the arguments to hook - terragrunt "${RUN_ALL_SUBCOMMAND[@]}" "${args[@]}" + "$tool_path" "${RUN_ALL_SUBCOMMAND[@]}" "${args[@]}" # return exit code to common::per_dir_hook local exit_code=$? diff --git a/hooks/terrascan.sh b/hooks/terrascan.sh index 277abca97..7b1229f3f 100755 --- a/hooks/terrascan.sh +++ b/hooks/terrascan.sh @@ -18,8 +18,10 @@ function main { done # JFYI: terrascan color already suppressed via PRE_COMMIT_COLOR=never + local -r tool_name="terrascan" + # shellcheck disable=SC2153 # False positive - common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" + common::per_dir_hook "$HOOK_ID" "$tool_name" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" } ####################################################################### @@ -33,7 +35,7 @@ function main { # Availability depends on hook. # parallelism_disabled (bool) if true - skip lock mechanism # args (array) arguments that configure wrapped tool behavior -# tf_path (string) PATH to Terraform/OpenTofu binary +# tool_path (string) resolved path to the wrapped tool's binary # Outputs: # If failed - print out hook checks status ####################################################################### @@ -44,13 +46,12 @@ function per_dir_hook_unique_part { local -r change_dir_in_unique_part="$2" # shellcheck disable=SC2034 # Unused var. local -r parallelism_disabled="$3" - # shellcheck disable=SC2034 # Unused var. - local -r tf_path="$4" + local -r tool_path="$4" shift 4 local -a -r args=("$@") # pass the arguments to hook - terrascan scan -i terraform "${args[@]}" + "$tool_path" scan -i terraform "${args[@]}" # return exit code to common::per_dir_hook local exit_code=$? @@ -61,13 +62,15 @@ function per_dir_hook_unique_part { # Unique part of `common::per_dir_hook`. The function is executed one time # in the root git repo # Arguments: +# tool_path (string) resolved path to the wrapped tool's binary # args (array) arguments that configure wrapped tool behavior ####################################################################### function run_hook_on_whole_repo { + local -r tool_path="$1" + shift local -a -r args=("$@") - # pass the arguments to hook - terrascan scan -i terraform "${args[@]}" + "$tool_path" scan -i terraform "${args[@]}" # return exit code to common::per_dir_hook local exit_code=$? diff --git a/hooks/tfupdate.sh b/hooks/tfupdate.sh index 1d474318b..337502593 100755 --- a/hooks/tfupdate.sh +++ b/hooks/tfupdate.sh @@ -25,8 +25,10 @@ function main { exit 1 fi + local -r tool_name="tfupdate" + # shellcheck disable=SC2153 # False positive - common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" + common::per_dir_hook "$HOOK_ID" "$tool_name" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" } ####################################################################### # Unique part of `common::per_dir_hook`. The function is executed in loop @@ -39,7 +41,7 @@ function main { # Availability depends on hook. # parallelism_disabled (bool) if true - skip lock mechanism # args (array) arguments that configure wrapped tool behavior -# tf_path (string) PATH to Terraform/OpenTofu binary +# tool_path (string) resolved path to the wrapped tool's binary # Outputs: # If failed - print out hook checks status ####################################################################### @@ -50,13 +52,12 @@ function per_dir_hook_unique_part { local -r change_dir_in_unique_part="$2" # shellcheck disable=SC2034 # Unused var. local -r parallelism_disabled="$3" - # shellcheck disable=SC2034 # Unused var. - local -r tf_path="$4" + local -r tool_path="$4" shift 4 local -a -r args=("$@") # pass the arguments to hook - tfupdate "${args[@]}" . + "$tool_path" "${args[@]}" . # return exit code to common::per_dir_hook local exit_code=$? @@ -67,13 +68,15 @@ function per_dir_hook_unique_part { # Unique part of `common::per_dir_hook`. The function is executed one time # in the root git repo # Arguments: +# tool_path (string) resolved path to the wrapped tool's binary # args (array) arguments that configure wrapped tool behavior ####################################################################### function run_hook_on_whole_repo { + local -r tool_path="$1" + shift local -a -r args=("$@") - # pass the arguments to hook - tfupdate "${args[@]}" --recursive . + "$tool_path" "${args[@]}" --recursive . # return exit code to common::per_dir_hook local exit_code=$? diff --git a/tools/install/_common.sh b/tools/install/_common.sh index eaaafa0cd..3a4d97da0 100755 --- a/tools/install/_common.sh +++ b/tools/install/_common.sh @@ -8,8 +8,12 @@ TOOL=${0##*/} readonly TOOL=${TOOL%%.*} # Get "TOOL_VERSION" -# shellcheck disable=SC1091 # Created in Dockerfile before execution of this script -source /.env +# /.env is created in the Dockerfile before this script runs there; when +# this script is invoked directly (e.g. at hook run-time, outside a Docker +# build), it won't exist and the version env var is expected to already be +# exported by the caller instead. +# shellcheck disable=SC1091 +[[ -f /.env ]] && source /.env env_var_name="${TOOL//-/_}" env_var_name="${env_var_name^^}_VERSION" # shellcheck disable=SC2034 # Used in other scripts