mirror of
https://github.com/tofuutils/pre-commit-opentofu.git
synced 2025-10-15 17:38:54 +02:00
362 lines
12 KiB
Bash
362 lines
12 KiB
Bash
#!/usr/bin/env bash
|
|
set -eo pipefail
|
|
|
|
# Hook ID, based on hook filename.
|
|
# Hook filename MUST BE same with `- id` in .pre-commit-hooks.yaml file
|
|
# shellcheck disable=SC2034 # Unused var.
|
|
HOOK_ID=${0##*/}
|
|
readonly HOOK_ID=${HOOK_ID%%.*}
|
|
|
|
#######################################################################
|
|
# Init arguments parser
|
|
# Arguments:
|
|
# script_dir - absolute path to hook dir location
|
|
#######################################################################
|
|
function common::initialize {
|
|
local -r script_dir=$1
|
|
# source getopt function
|
|
# shellcheck source=../lib_getopt
|
|
. "$script_dir/../lib_getopt"
|
|
}
|
|
|
|
#######################################################################
|
|
# Parse args and filenames passed to script and populate respective
|
|
# global variables with appropriate values
|
|
# Globals (init and populate):
|
|
# ARGS (array) arguments that configure wrapped tool behavior
|
|
# HOOK_CONFIG (array) arguments that configure hook behavior
|
|
# TF_INIT_ARGS (array) arguments for `terraform init` command
|
|
# ENV_VARS (array) environment variables will be available
|
|
# for all 3rd-party tools executed by a hook.
|
|
# FILES (array) filenames to check
|
|
# Arguments:
|
|
# $@ (array) all specified in `hooks.[].args` in
|
|
# `.pre-commit-config.yaml` and filenames.
|
|
#######################################################################
|
|
function common::parse_cmdline {
|
|
# common global arrays.
|
|
# Populated via `common::parse_cmdline` and can be used inside hooks' functions
|
|
ARGS=() HOOK_CONFIG=() FILES=()
|
|
# Used inside `common::terraform_init` function
|
|
TF_INIT_ARGS=()
|
|
# Used inside `common::export_provided_env_vars` function
|
|
ENV_VARS=()
|
|
|
|
local argv
|
|
# TODO: Planned breaking change: remove `init-args`, `envs` as not self-descriptive
|
|
argv=$(getopt -o a:,h:,i:,e: --long args:,hook-config:,init-args:,tf-init-args:,envs:,env-vars: -- "$@") || return
|
|
eval "set -- $argv"
|
|
|
|
for argv; do
|
|
case $argv in
|
|
-a | --args)
|
|
shift
|
|
# `argv` is an string from array with content like:
|
|
# ('provider aws' '--version "> 0.14"' '--ignore-path "some/path"')
|
|
# where each element is the value of each `--args` from hook config.
|
|
# `echo` prints contents of `argv` as an expanded string
|
|
# `xargs` passes expanded string to `printf`
|
|
# `printf` which splits it into NUL-separated elements,
|
|
# NUL-separated elements read by `read` using empty separator
|
|
# (`-d ''` or `-d $'\0'`)
|
|
# into an `ARGS` array
|
|
|
|
# This allows to "rebuild" initial `args` array of sort of grouped elements
|
|
# into a proper array, where each element is a standalone array slice
|
|
# with quoted elements being treated as a standalone slice of array as well.
|
|
while read -r -d '' ARG; do
|
|
ARGS+=("$ARG")
|
|
done < <(echo "$1" | xargs printf '%s\0')
|
|
shift
|
|
;;
|
|
-h | --hook-config)
|
|
shift
|
|
HOOK_CONFIG+=("$1;")
|
|
shift
|
|
;;
|
|
# TODO: Planned breaking change: remove `--init-args` as not self-descriptive
|
|
-i | --init-args | --tf-init-args)
|
|
shift
|
|
TF_INIT_ARGS+=("$1")
|
|
shift
|
|
;;
|
|
# TODO: Planned breaking change: remove `--envs` as not self-descriptive
|
|
-e | --envs | --env-vars)
|
|
shift
|
|
ENV_VARS+=("$1")
|
|
shift
|
|
;;
|
|
--)
|
|
shift
|
|
# shellcheck disable=SC2034 # Variable is used
|
|
FILES=("$@")
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
#######################################################################
|
|
# Expand environment variables definition into their values in '--args'.
|
|
# Support expansion only for ${ENV_VAR} vars, not $ENV_VAR.
|
|
# Globals (modify):
|
|
# ARGS (array) arguments that configure wrapped tool behavior
|
|
#######################################################################
|
|
function common::parse_and_export_env_vars {
|
|
local arg_idx
|
|
|
|
for arg_idx in "${!ARGS[@]}"; do
|
|
local arg="${ARGS[$arg_idx]}"
|
|
|
|
# Repeat until all env vars will be expanded
|
|
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
|
|
# Get `ENV_VAR` from `.*${ENV_VAR}.*`
|
|
local env_var_name=${arg#*$\{}
|
|
env_var_name=${env_var_name%%\}*}
|
|
local env_var_value="${!env_var_name}"
|
|
# shellcheck disable=SC2016 # '${' should not be expanded
|
|
common::colorify "green" 'Found ${'"$env_var_name"'} in: '"'$arg'"
|
|
# Replace env var name with its value.
|
|
# `$arg` will be checked in `if` conditional, `$ARGS` will be used in the next functions.
|
|
# shellcheck disable=SC2016 # '${' should not be expanded
|
|
arg=${arg/'${'$env_var_name'}'/$env_var_value}
|
|
ARGS[$arg_idx]=$arg
|
|
# shellcheck disable=SC2016 # '${' should not be expanded
|
|
common::colorify "green" 'After ${'"$env_var_name"'} expansion: '"'$arg'\n"
|
|
continue
|
|
fi
|
|
break
|
|
done
|
|
done
|
|
}
|
|
|
|
#######################################################################
|
|
# This is a workaround to improve performance when all files are passed
|
|
# See: https://github.com/antonbabenko/pre-commit-terraform/issues/309
|
|
# Arguments:
|
|
# hook_id (string) hook ID, see `- id` for details in .pre-commit-hooks.yaml file
|
|
# files (array) filenames to check
|
|
# Outputs:
|
|
# Return 0 if `-a|--all` arg was passed to `pre-commit`
|
|
#######################################################################
|
|
function common::is_hook_run_on_whole_repo {
|
|
local -r hook_id="$1"
|
|
shift
|
|
local -a -r files=("$@")
|
|
# get directory containing `.pre-commit-hooks.yaml` file
|
|
local -r root_config_dir="$(dirname "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)")"
|
|
# get included and excluded files from .pre-commit-hooks.yaml file
|
|
local -r hook_config_block=$(sed -n "/^- id: $hook_id$/,/^$/p" "$root_config_dir/.pre-commit-hooks.yaml")
|
|
local -r included_files=$(awk '$1 == "files:" {print $2; exit}' <<< "$hook_config_block")
|
|
local -r excluded_files=$(awk '$1 == "exclude:" {print $2; exit}' <<< "$hook_config_block")
|
|
# sorted string with the files passed to the hook by pre-commit
|
|
local -r files_to_check=$(printf '%s\n' "${files[@]}" | sort | tr '\n' ' ')
|
|
# git ls-files sorted string
|
|
local all_files_that_can_be_checked
|
|
|
|
if [ -z "$excluded_files" ]; then
|
|
all_files_that_can_be_checked=$(git ls-files | sort | grep -e "$included_files" | tr '\n' ' ')
|
|
else
|
|
all_files_that_can_be_checked=$(git ls-files | sort | grep -e "$included_files" | grep -v -e "$excluded_files" | tr '\n' ' ')
|
|
fi
|
|
|
|
if [ "$files_to_check" == "$all_files_that_can_be_checked" ]; then
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
#######################################################################
|
|
# Hook execution boilerplate logic which is common to hooks, that run
|
|
# on per dir basis.
|
|
# 1. Because hook runs on whole dir, reduce file paths to uniq dir paths
|
|
# 2. Run for each dir `per_dir_hook_unique_part`, on all paths
|
|
# 2.1. If at least 1 check failed - change exit code to non-zero
|
|
# 3. Complete hook execution and return exit code
|
|
# Arguments:
|
|
# hook_id (string) hook ID, see `- id` for details in .pre-commit-hooks.yaml file
|
|
# 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 -a args=()
|
|
# Expand args to a true array.
|
|
# Based on https://stackoverflow.com/a/10953834
|
|
while ((args_array_length-- > 0)); do
|
|
args+=("$1")
|
|
shift
|
|
done
|
|
# assign rest of function's positional ARGS into `files` array,
|
|
# despite there's only one positional ARG left
|
|
local -a -r files=("$@")
|
|
|
|
# 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[@]}"
|
|
exit 0
|
|
fi
|
|
|
|
# consume modified files passed from pre-commit so that
|
|
# hook runs against only those relevant directories
|
|
local index=0
|
|
for file_with_path in "${files[@]}"; do
|
|
file_with_path="${file_with_path// /__REPLACED__SPACE__}"
|
|
|
|
dir_paths[index]=$(dirname "$file_with_path")
|
|
|
|
((index += 1))
|
|
done
|
|
|
|
# Lookup hook-config for modifiers that impact common behavior
|
|
local change_dir_in_unique_part=false
|
|
IFS=";" read -r -a configs <<< "${HOOK_CONFIG[*]}"
|
|
for c in "${configs[@]}"; do
|
|
IFS="=" read -r -a config <<< "$c"
|
|
key=${config[0]}
|
|
value=${config[1]}
|
|
|
|
case $key in
|
|
--delegate-chdir)
|
|
# this flag will skip pushing and popping directories
|
|
# delegating the responsibility to the hooked plugin/binary
|
|
if [[ ! $value || $value == true ]]; then
|
|
change_dir_in_unique_part="delegate_chdir"
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# preserve errexit status
|
|
shopt -qo errexit && ERREXIT_IS_SET=true
|
|
# allow hook to continue if exit_code is greater than 0
|
|
set +e
|
|
local final_exit_code=0
|
|
|
|
# run hook for each path
|
|
for dir_path in $(echo "${dir_paths[*]}" | tr ' ' '\n' | sort -u); do
|
|
dir_path="${dir_path//__REPLACED__SPACE__/ }"
|
|
|
|
if [[ $change_dir_in_unique_part == false ]]; then
|
|
pushd "$dir_path" > /dev/null || continue
|
|
fi
|
|
|
|
per_dir_hook_unique_part "$dir_path" "$change_dir_in_unique_part" "${args[@]}"
|
|
|
|
local exit_code=$?
|
|
if [ $exit_code -ne 0 ]; then
|
|
final_exit_code=$exit_code
|
|
fi
|
|
|
|
if [[ $change_dir_in_unique_part == false ]]; then
|
|
popd > /dev/null
|
|
fi
|
|
|
|
done
|
|
|
|
# restore errexit if it was set before the "for" loop
|
|
[[ $ERREXIT_IS_SET ]] && set -e
|
|
# return the hook final exit_code
|
|
exit $final_exit_code
|
|
}
|
|
|
|
#######################################################################
|
|
# Colorize provided string and print it out to stdout
|
|
# Environment variables:
|
|
# PRE_COMMIT_COLOR (string) If set to `never` - do not colorize output
|
|
# Arguments:
|
|
# COLOR (string) Color name that will be used to colorize
|
|
# TEXT (string)
|
|
# Outputs:
|
|
# Print out provided text to stdout
|
|
#######################################################################
|
|
function common::colorify {
|
|
# shellcheck disable=SC2034
|
|
local -r red="\x1b[0m\x1b[31m"
|
|
# shellcheck disable=SC2034
|
|
local -r green="\x1b[0m\x1b[32m"
|
|
# shellcheck disable=SC2034
|
|
local -r yellow="\x1b[0m\x1b[33m"
|
|
# Color reset
|
|
local -r RESET="\x1b[0m"
|
|
|
|
# Params start #
|
|
local COLOR="${!1}"
|
|
local -r TEXT=$2
|
|
# Params end #
|
|
|
|
if [ "$PRE_COMMIT_COLOR" = "never" ]; then
|
|
COLOR=$RESET
|
|
fi
|
|
|
|
echo -e "${COLOR}${TEXT}${RESET}"
|
|
}
|
|
|
|
#######################################################################
|
|
# Run terraform init command
|
|
# Arguments:
|
|
# command_name (string) command that will tun after successful init
|
|
# dir_path (string) PATH to dir relative to git repo root.
|
|
# Can be used in error logging
|
|
# Globals (init and populate):
|
|
# TF_INIT_ARGS (array) arguments for `terraform init` command
|
|
# Outputs:
|
|
# If failed - print out terraform init output
|
|
#######################################################################
|
|
# TODO: v2.0: Move it inside terraform_validate.sh
|
|
function common::terraform_init {
|
|
local -r command_name=$1
|
|
local -r dir_path=$2
|
|
|
|
local exit_code=0
|
|
local init_output
|
|
|
|
# Suppress terraform init color
|
|
if [ "$PRE_COMMIT_COLOR" = "never" ]; then
|
|
TF_INIT_ARGS+=("-no-color")
|
|
fi
|
|
|
|
if [ ! -d .terraform/modules ] || [ ! -d .terraform/providers ]; then
|
|
init_output=$(terraform init -backend=false "${TF_INIT_ARGS[@]}" 2>&1)
|
|
exit_code=$?
|
|
|
|
if [ $exit_code -ne 0 ]; then
|
|
common::colorify "red" "'terraform init' failed, '$command_name' skipped: $dir_path"
|
|
echo -e "$init_output\n\n"
|
|
else
|
|
common::colorify "green" "Command 'terraform init' successfully done: $dir_path"
|
|
fi
|
|
fi
|
|
|
|
return $exit_code
|
|
}
|
|
|
|
#######################################################################
|
|
# Export provided K/V as environment variables.
|
|
# Arguments:
|
|
# env_vars (array) environment variables will be available
|
|
# for all 3rd-party tools executed by a hook.
|
|
#######################################################################
|
|
function common::export_provided_env_vars {
|
|
local -a -r env_vars=("$@")
|
|
|
|
local var
|
|
local var_name
|
|
local var_value
|
|
|
|
for var in "${env_vars[@]}"; do
|
|
var_name="${var%%=*}"
|
|
var_value="${var#*=}"
|
|
# shellcheck disable=SC2086
|
|
export $var_name="$var_value"
|
|
done
|
|
}
|