From 1f16f09c6fb7c76af53a48dfce33626197615d51 Mon Sep 17 00:00:00 2001 From: Maksym Vlasov Date: Thu, 6 Jan 2022 13:21:52 +0200 Subject: [PATCH] chore: Refactor all hooks (#310) --- infracost_breakdown.sh | 20 +++-- terraform_docs.sh | 66 ++++++++--------- terraform_fmt.sh | 114 ++++++++++++++++++---------- terraform_providers_lock.sh | 139 +++++++++++++++++++++++----------- terraform_tflint.sh | 137 +++++++++++++++++++++++----------- terraform_tfsec.sh | 127 +++++++++++++++++++------------ terraform_validate.sh | 48 +++++------- terragrunt_fmt.sh | 112 ++++++++++++++++++++++++---- terragrunt_validate.sh | 112 ++++++++++++++++++++++++---- terrascan.sh | 144 +++++++++++++++++++----------------- 10 files changed, 677 insertions(+), 342 deletions(-) diff --git a/infracost_breakdown.sh b/infracost_breakdown.sh index ac0fdf9..c56e4cd 100755 --- a/infracost_breakdown.sh +++ b/infracost_breakdown.sh @@ -8,15 +8,14 @@ function main { } function common::colorify { - # Colors. Provided as first string to first arg of function. # shellcheck disable=SC2034 - local -r red="$(tput setaf 1)" + local -r red="\e[0m\e[31m" # shellcheck disable=SC2034 - local -r green="$(tput setaf 2)" + local -r green="\e[0m\e[32m" # shellcheck disable=SC2034 - local -r yellow="$(tput setaf 3)" + local -r yellow="\e[0m\e[33m" # Color reset - local -r RESET="$(tput sgr0)" + local -r RESET="\e[0m" # Params start # local COLOR="${!1}" @@ -40,12 +39,11 @@ function common::initialize { . "$SCRIPT_DIR/lib_getopt" } -# common global arrays. -# Populated in `parse_cmdline` and can used in hooks functions -declare -a ARGS=() -declare -a HOOK_CONFIG=() -declare -a FILES=() function common::parse_cmdline { + # common global arrays. + # Populated via `common::parse_cmdline` and can be used inside hooks' functions + declare -g -a ARGS=() FILES=() HOOK_CONFIG=() + local argv argv=$(getopt -o a:,h: --long args:,hook-config: -- "$@") || return eval "set -- $argv" @@ -180,4 +178,4 @@ function infracost_breakdown_ { fi } -[[ ${BASH_SOURCE[0]} != "$0" ]] || main "$@" +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" diff --git a/terraform_docs.sh b/terraform_docs.sh index 3b04afe..0c1e711 100755 --- a/terraform_docs.sh +++ b/terraform_docs.sh @@ -1,35 +1,31 @@ #!/usr/bin/env bash set -eo pipefail -main() { - initialize_ - parse_cmdline_ "$@" +function main { + common::initialize + common::parse_cmdline "$@" # Support for setting relative PATH to .terraform-docs.yml config. ARGS=${ARGS[*]/--config=/--config=$(pwd)\/} terraform_docs_ "${HOOK_CONFIG[*]}" "$ARGS" "${FILES[@]}" } -initialize_() { +function common::initialize { + local SCRIPT_DIR # get directory containing this script - local dir - local source - source="${BASH_SOURCE[0]}" - while [[ -L $source ]]; do # resolve $source until the file is no longer a symlink - dir="$(cd -P "$(dirname "$source")" > /dev/null && pwd)" - source="$(readlink "$source")" - # if $source was a relative symlink, we need to resolve it relative to the path where the symlink file was located - [[ $source != /* ]] && source="$dir/$source" - done - _SCRIPT_DIR="$(dirname "$source")" + SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" # source getopt function # shellcheck source=lib_getopt - . "$_SCRIPT_DIR/lib_getopt" + . "$SCRIPT_DIR/lib_getopt" } -parse_cmdline_() { - declare argv - argv=$(getopt -o a: --long args:,hook-config: -- "$@") || return +function common::parse_cmdline { + # common global arrays. + # Populated via `common::parse_cmdline` and can be used inside hooks' functions + declare -g -a ARGS=() FILES=() HOOK_CONFIG=() + + local argv + argv=$(getopt -o a:,h: --long args:,hook-config: -- "$@") || return eval "set -- $argv" for argv; do @@ -39,9 +35,9 @@ parse_cmdline_() { ARGS+=("$1") shift ;; - --hook-config) + -h | --hook-config) shift - HOOK_CONFIG+=("$1") + HOOK_CONFIG+=("$1;") shift ;; --) @@ -53,12 +49,15 @@ parse_cmdline_() { done } -terraform_docs_() { +function terraform_docs_ { local -r hook_config="$1" local -r args="$2" shift 2 local -a -r files=("$@") + # Get hook settings + IFS=";" read -r -a configs <<< "$hook_config" + local hack_terraform_docs hack_terraform_docs=$(terraform version | sed -n 1p | grep -c 0.12) || true @@ -72,7 +71,7 @@ terraform_docs_() { if [[ -z "$is_old_terraform_docs" ]]; then # Using terraform-docs 0.8+ (preferred) - terraform_docs "0" "$hook_config" "$args" "${files[@]}" + terraform_docs "0" "${configs[*]}" "$args" "${files[@]}" elif [[ "$hack_terraform_docs" == "1" ]]; then # Using awk script because terraform-docs is older than 0.8 and terraform 0.12 is used @@ -84,17 +83,17 @@ terraform_docs_() { local tmp_file_awk tmp_file_awk=$(mktemp "${TMPDIR:-/tmp}/terraform-docs-XXXXXXXXXX") terraform_docs_awk "$tmp_file_awk" - terraform_docs "$tmp_file_awk" "$hook_config" "$args" "${files[@]}" + terraform_docs "$tmp_file_awk" "${configs[*]}" "$args" "${files[@]}" rm -f "$tmp_file_awk" else # Using terraform 0.11 and no awk script is needed for that - terraform_docs "0" "$hook_config" "$args" "${files[@]}" + terraform_docs "0" "${configs[*]}" "$args" "${files[@]}" fi } -terraform_docs() { +function terraform_docs { local -r terraform_docs_awk_file="$1" local -r hook_config="$2" local -r args="$3" @@ -141,11 +140,11 @@ terraform_docs() { esac done - local path_uniq - for path_uniq in $(echo "${paths[*]}" | tr ' ' '\n' | sort -u); do - path_uniq="${path_uniq//__REPLACED__SPACE__/ }" + local dir_path + for dir_path in $(echo "${paths[*]}" | tr ' ' '\n' | sort -u); do + dir_path="${dir_path//__REPLACED__SPACE__/ }" - pushd "$path_uniq" > /dev/null + pushd "$dir_path" > /dev/null || continue # # Create file if it not exist and `--create-if-not-exist=true` provided @@ -212,7 +211,7 @@ terraform_docs() { done } -terraform_docs_awk() { +function terraform_docs_awk { local -r output_file=$1 cat << "EOF" > "$output_file" @@ -371,9 +370,4 @@ EOF } -# global arrays -declare -a ARGS=() -declare -a FILES=() -declare -a HOOK_CONFIG=() - -[[ ${BASH_SOURCE[0]} != "$0" ]] || main "$@" +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" diff --git a/terraform_fmt.sh b/terraform_fmt.sh index 82e66f5..f6cad78 100755 --- a/terraform_fmt.sh +++ b/terraform_fmt.sh @@ -1,33 +1,29 @@ #!/usr/bin/env bash set -eo pipefail -main() { - initialize_ - parse_cmdline_ "$@" - terraform_fmt_ +function main { + common::initialize + common::parse_cmdline "$@" + terraform_fmt_ "${ARGS[*]}" "${FILES[@]}" } -initialize_() { +function common::initialize { + local SCRIPT_DIR # get directory containing this script - local dir - local source - source="${BASH_SOURCE[0]}" - while [[ -L $source ]]; do # resolve $source until the file is no longer a symlink - dir="$(cd -P "$(dirname "$source")" > /dev/null && pwd)" - source="$(readlink "$source")" - # if $source was a relative symlink, we need to resolve it relative to the path where the symlink file was located - [[ $source != /* ]] && source="$dir/$source" - done - _SCRIPT_DIR="$(dirname "$source")" + SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" # source getopt function # shellcheck source=lib_getopt - . "$_SCRIPT_DIR/lib_getopt" + . "$SCRIPT_DIR/lib_getopt" } -parse_cmdline_() { - declare argv - argv=$(getopt -o a: --long args: -- "$@") || return +function common::parse_cmdline { + # common global arrays. + # Populated via `common::parse_cmdline` and can be used inside hooks' functions + declare -g -a ARGS=() FILES=() HOOK_CONFIG=() + + local argv + argv=$(getopt -o a:,h: --long args:,hook-config: -- "$@") || return eval "set -- $argv" for argv; do @@ -37,6 +33,11 @@ parse_cmdline_() { ARGS+=("$1") shift ;; + -h | --hook-config) + shift + HOOK_CONFIG+=("$1;") + shift + ;; --) shift FILES=("$@") @@ -46,44 +47,77 @@ parse_cmdline_() { done } -terraform_fmt_() { - - declare -a paths - declare -a tfvars_files - - index=0 - - for file_with_path in "${FILES[@]}"; do +function terraform_fmt_ { + local -r args="$1" + shift 1 + local -a -r files=("$@") + # 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__}" - paths[index]=$(dirname "$file_with_path") - + dir_paths[index]=$(dirname "$file_with_path") + # TODO Unique part if [[ "$file_with_path" == *".tfvars" ]]; then tfvars_files+=("$file_with_path") fi - + #? End for unique part ((index += 1)) done - for path_uniq in $(echo "${paths[*]}" | tr ' ' '\n' | sort -u); do - path_uniq="${path_uniq//__REPLACED__SPACE__/ }" + # 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 - ( - cd "$path_uniq" - terraform fmt "${ARGS[@]}" - ) + # run hook for each path + for dir_path in $(echo "${dir_paths[*]}" | tr ' ' '\n' | sort -u); do + dir_path="${dir_path//__REPLACED__SPACE__/ }" + pushd "$dir_path" > /dev/null || continue + + per_dir_hook_unique_part "$args" "$dir_path" + + local exit_code=$? + if [ $exit_code -ne 0 ]; then + final_exit_code=$exit_code + fi + + popd > /dev/null done + # TODO: Unique part # terraform.tfvars are excluded by `terraform fmt` for tfvars_file in "${tfvars_files[@]}"; do tfvars_file="${tfvars_file//__REPLACED__SPACE__/ }" terraform fmt "${ARGS[@]}" "$tfvars_file" + local exit_code=$? + if [ $exit_code -ne 0 ]; then + final_exit_code=$exit_code + fi done + #? End for unique part + # 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 + } -# global arrays -declare -a ARGS=() -declare -a FILES=() +function per_dir_hook_unique_part { + # common logic located in common::per_dir_hook + local -r args="$1" + local -r dir_path="$2" -[[ ${BASH_SOURCE[0]} != "$0" ]] || main "$@" + # pass the arguments to hook + # shellcheck disable=SC2068 # hook fails when quoting is used ("$arg[@]") + terraform fmt ${args[@]} + + # return exit code to common::per_dir_hook + local exit_code=$? + return $exit_code +} + +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" diff --git a/terraform_providers_lock.sh b/terraform_providers_lock.sh index 31ea63f..26bf40b 100755 --- a/terraform_providers_lock.sh +++ b/terraform_providers_lock.sh @@ -2,33 +2,51 @@ set -eo pipefail -main() { - initialize_ - parse_cmdline_ "$@" - terraform_providers_lock_ +function main { + common::initialize + common::parse_cmdline "$@" + common::per_dir_hook "${ARGS[*]}" "${FILES[@]}" } -initialize_() { +function common::colorify { + # shellcheck disable=SC2034 + local -r red="\e[0m\e[31m" + # shellcheck disable=SC2034 + local -r green="\e[0m\e[32m" + # shellcheck disable=SC2034 + local -r yellow="\e[0m\e[33m" + # Color reset + local -r RESET="\e[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}" +} + +function common::initialize { + local SCRIPT_DIR # get directory containing this script - local dir - local source - source="${BASH_SOURCE[0]}" - while [[ -L $source ]]; do # resolve $source until the file is no longer a symlink - dir="$(cd -P "$(dirname "$source")" > /dev/null && pwd)" - source="$(readlink "$source")" - # if $source was a relative symlink, we need to resolve it relative to the path where the symlink file was located - [[ $source != /* ]] && source="$dir/$source" - done - _SCRIPT_DIR="$(dirname "$source")" + SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" # source getopt function # shellcheck source=lib_getopt - . "$_SCRIPT_DIR/lib_getopt" + . "$SCRIPT_DIR/lib_getopt" } -parse_cmdline_() { - declare argv - argv=$(getopt -o a: --long args: -- "$@") || return +function common::parse_cmdline { + # common global arrays. + # Populated via `common::parse_cmdline` and can be used inside hooks' functions + declare -g -a ARGS=() FILES=() HOOK_CONFIG=() + + local argv + argv=$(getopt -o a:,h: --long args:,hook-config: -- "$@") || return eval "set -- $argv" for argv; do @@ -38,6 +56,11 @@ parse_cmdline_() { ARGS+=("$1") shift ;; + -h | --hook-config) + shift + HOOK_CONFIG+=("$1;") + shift + ;; --) shift FILES=("$@") @@ -47,42 +70,72 @@ parse_cmdline_() { done } -terraform_providers_lock_() { - local -a paths - local index=0 - local file_with_path +function common::per_dir_hook { + local -r args="$1" + shift 1 + local -a -r files=("$@") - for file_with_path in "${FILES[@]}"; do + # 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__}" - paths[index]=$(dirname "$file_with_path") + dir_paths[index]=$(dirname "$file_with_path") ((index += 1)) done - local path_uniq - for path_uniq in $(echo "${paths[*]}" | tr ' ' '\n' | sort -u); do - path_uniq="${path_uniq//__REPLACED__SPACE__/ }" + # 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 - if [[ ! -d "${path_uniq}/.terraform" ]]; then - set +e - init_output=$(terraform -chdir="${path_uniq}" init -backend=false 2>&1) - init_code=$? - set -e + # run hook for each path + for dir_path in $(echo "${dir_paths[*]}" | tr ' ' '\n' | sort -u); do + dir_path="${dir_path//__REPLACED__SPACE__/ }" + pushd "$dir_path" > /dev/null || continue - if [[ $init_code != 0 ]]; then - echo "Init before validation failed: $path_uniq" - echo "$init_output" - exit 1 - fi + per_dir_hook_unique_part "$args" "$dir_path" + + local exit_code=$? + if [ $exit_code -ne 0 ]; then + final_exit_code=$exit_code fi - terraform -chdir="${path_uniq}" providers lock "${ARGS[@]}" + popd > /dev/null 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 } -# global arrays -declare -a ARGS -declare -a FILES +function per_dir_hook_unique_part { + # common logic located in common::per_dir_hook + local -r args="$1" + local -r dir_path="$2" -[[ ${BASH_SOURCE[0]} != "$0" ]] || main "$@" + if [ ! -d ".terraform" ]; then + init_output=$(terraform init -backend=false 2>&1) + init_code=$? + + if [ $init_code -ne 0 ]; then + common::colorify "red" "Init before validation failed: $dir_path" + common::colorify "red" "$init_output" + exit $init_code + fi + fi + + # pass the arguments to hook + # shellcheck disable=SC2068 # hook fails when quoting is used ("$arg[@]") + terraform providers lock ${args[@]} + + # return exit code to common::per_dir_hook + local exit_code=$? + return $exit_code +} + +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" diff --git a/terraform_tflint.sh b/terraform_tflint.sh index 87c1203..d6501b8 100755 --- a/terraform_tflint.sh +++ b/terraform_tflint.sh @@ -2,41 +2,65 @@ set -eo pipefail -main() { - initialize_ - parse_cmdline_ "$@" - tflint_ +function main { + common::initialize + common::parse_cmdline "$@" + # Support for setting PATH to repo root. + ARGS=${ARGS[*]/__GIT_WORKING_DIR__/$(pwd)\/} + common::per_dir_hook "$ARGS" "${FILES[@]}" } -initialize_() { +function common::colorify { + # shellcheck disable=SC2034 + local -r red="\e[0m\e[31m" + # shellcheck disable=SC2034 + local -r green="\e[0m\e[32m" + # shellcheck disable=SC2034 + local -r yellow="\e[0m\e[33m" + # Color reset + local -r RESET="\e[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}" +} + +function common::initialize { + local SCRIPT_DIR # get directory containing this script - local dir - local source - source="${BASH_SOURCE[0]}" - while [[ -L $source ]]; do # resolve $source until the file is no longer a symlink - dir="$(cd -P "$(dirname "$source")" > /dev/null && pwd)" - source="$(readlink "$source")" - # if $source was a relative symlink, we need to resolve it relative to the path where the symlink file was located - [[ $source != /* ]] && source="$dir/$source" - done - _SCRIPT_DIR="$(dirname "$source")" + SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" # source getopt function # shellcheck source=lib_getopt - . "$_SCRIPT_DIR/lib_getopt" + . "$SCRIPT_DIR/lib_getopt" } -parse_cmdline_() { - declare argv - argv=$(getopt -o a: --long args: -- "$@") || return +function common::parse_cmdline { + # common global arrays. + # Populated via `common::parse_cmdline` and can be used inside hooks' functions + declare -g -a ARGS=() FILES=() HOOK_CONFIG=() + + local argv + argv=$(getopt -o a:,h: --long args:,hook-config: -- "$@") || return eval "set -- $argv" for argv; do case $argv in -a | --args) shift - expanded_arg="${1//__GIT_WORKING_DIR__/$PWD}" - ARGS+=("$expanded_arg") + ARGS+=("$1") + shift + ;; + -h | --hook-config) + shift + HOOK_CONFIG+=("$1;") shift ;; --) @@ -46,43 +70,68 @@ parse_cmdline_() { ;; esac done - } -tflint_() { +function common::per_dir_hook { + local -r args="$1" + shift 1 + local -a -r files=("$@") + + # 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 + for file_with_path in "${files[@]}"; do file_with_path="${file_with_path// /__REPLACED__SPACE__}" - paths[index]=$(dirname "$file_with_path") + dir_paths[index]=$(dirname "$file_with_path") ((index += 1)) done - set +e - tflint_final_exit_code=0 - for path_uniq in $(echo "${paths[*]}" | tr ' ' '\n' | sort -u); do - path_uniq="${path_uniq//__REPLACED__SPACE__/ }" - pushd "$path_uniq" > /dev/null - # Print checked PATH **only** if TFLint have any messages - # shellcheck disable=SC2091 # Suppress error output - $(tflint "${ARGS[@]}" 2>&1) 2> /dev/null || { - echo >&2 -e "\033[1;33m\nTFLint in $path_uniq/:\033[0m" - tflint "${ARGS[@]}" - } + # 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__/ }" + pushd "$dir_path" > /dev/null || continue + + per_dir_hook_unique_part "$args" "$dir_path" + local exit_code=$? - if [ $exit_code != 0 ]; then - tflint_final_exit_code=$exit_code + if [ $exit_code -ne 0 ]; then + final_exit_code=$exit_code fi popd > /dev/null done - set -e - exit $tflint_final_exit_code + + # 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 } -# global arrays -declare -a ARGS -declare -a FILES +function per_dir_hook_unique_part { + # common logic located in common::per_dir_hook + local -r args="$1" + local -r dir_path="$2" -[[ ${BASH_SOURCE[0]} != "$0" ]] || main "$@" + # Print checked PATH **only** if TFLint have any messages + # shellcheck disable=SC2091,SC2068 # Suppress error output + $(tflint ${args[@]} 2>&1) 2> /dev/null || { + common::colorify "yellow" "TFLint in $dir_path/:" + + # shellcheck disable=SC2068 # hook fails when quoting is used ("$arg[@]") + tflint ${args[@]} + } + + # return exit code to common::per_dir_hook + local exit_code=$? + return $exit_code +} + +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" diff --git a/terraform_tfsec.sh b/terraform_tfsec.sh index 0dc01fe..a68905b 100755 --- a/terraform_tfsec.sh +++ b/terraform_tfsec.sh @@ -1,74 +1,109 @@ #!/usr/bin/env bash set -eo pipefail -main() { - initialize_ - parse_cmdline_ "$@" - - # propagate $FILES to custom function - tfsec_ "$ARGS" "${FILES[*]}" +function main { + common::initialize + common::parse_cmdline "$@" + # Support for setting PATH to repo root. + ARGS=${ARGS[*]/__GIT_WORKING_DIR__/$(pwd)\/} + common::per_dir_hook "$ARGS" "${FILES[@]}" } -tfsec_() { - # consume modified files passed from pre-commit so that - # tfsec runs against only those relevant directories - for file_with_path in ${FILES[*]}; do - file_with_path="${file_with_path// /__REPLACED__SPACE__}" - paths[index]=$(dirname "$file_with_path") - - let "index+=1" - done - - for path_uniq in $(echo "${paths[*]}" | tr ' ' '\n' | sort -u); do - path_uniq="${path_uniq//__REPLACED__SPACE__/ }" - pushd "$path_uniq" > /dev/null - tfsec $ARGS - popd > /dev/null - done -} - -initialize_() { +function common::initialize { + local SCRIPT_DIR # get directory containing this script - local dir - local source - source="${BASH_SOURCE[0]}" - while [[ -L $source ]]; do # resolve $source until the file is no longer a symlink - dir="$(cd -P "$(dirname "$source")" > /dev/null && pwd)" - source="$(readlink "$source")" - # if $source was a relative symlink, we need to resolve it relative to the path where the symlink file was located - [[ $source != /* ]] && source="$dir/$source" - done - _SCRIPT_DIR="$(dirname "$source")" + SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" # source getopt function # shellcheck source=lib_getopt - . "$_SCRIPT_DIR/lib_getopt" + . "$SCRIPT_DIR/lib_getopt" } -parse_cmdline_() { - declare argv - argv=$(getopt -o a: --long args: -- "$@") || return +function common::parse_cmdline { + # common global arrays. + # Populated via `common::parse_cmdline` and can be used inside hooks' functions + declare -g -a ARGS=() FILES=() HOOK_CONFIG=() + + local argv + argv=$(getopt -o a:,h: --long args:,hook-config: -- "$@") || return eval "set -- $argv" for argv; do case $argv in -a | --args) shift - expanded_arg="${1//__GIT_WORKING_DIR__/$PWD}" - ARGS+=("$expanded_arg") + ARGS+=("$1") + shift + ;; + -h | --hook-config) + shift + HOOK_CONFIG+=("$1;") shift ;; --) shift - FILES+=("$@") + FILES=("$@") break ;; esac done } -# global arrays -declare -a ARGS=() -declare -a FILES=() +function common::per_dir_hook { + local -r args="$1" + shift 1 + local -a -r files=("$@") -[[ ${BASH_SOURCE[0]} != "$0" ]] || main "$@" + # 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 + + # 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__/ }" + pushd "$dir_path" > /dev/null || continue + + per_dir_hook_unique_part "$args" "$dir_path" + + local exit_code=$? + if [ $exit_code -ne 0 ]; then + final_exit_code=$exit_code + fi + + popd > /dev/null + 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 +} + +function per_dir_hook_unique_part { + # common logic located in common::per_dir_hook + local -r args="$1" + local -r dir_path="$2" + + # pass the arguments to hook + # shellcheck disable=SC2068 # hook fails when quoting is used ("$arg[@]") + tfsec ${args[@]} + + # return exit code to common::per_dir_hook + local exit_code=$? + return $exit_code +} + +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" diff --git a/terraform_validate.sh b/terraform_validate.sh index ea23dac..655bc09 100755 --- a/terraform_validate.sh +++ b/terraform_validate.sh @@ -4,31 +4,23 @@ set -eo pipefail # `terraform validate` requires this env variable to be set export AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION:-us-east-1} -main() { - initialize_ +function main { + common::initialize parse_cmdline_ "$@" terraform_validate_ } -initialize_() { +function common::initialize { + local SCRIPT_DIR # get directory containing this script - local dir - local source - source="${BASH_SOURCE[0]}" - while [[ -L $source ]]; do # resolve $source until the file is no longer a symlink - dir="$(cd -P "$(dirname "$source")" > /dev/null && pwd)" - source="$(readlink "$source")" - # if $source was a relative symlink, we need to resolve it relative to the path where the symlink file was located - [[ $source != /* ]] && source="$dir/$source" - done - _SCRIPT_DIR="$(dirname "$source")" + SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" # source getopt function # shellcheck source=lib_getopt - . "$_SCRIPT_DIR/lib_getopt" + . "$SCRIPT_DIR/lib_getopt" } -parse_cmdline_() { +function parse_cmdline_ { declare argv argv=$(getopt -o e:i:a: --long envs:,init-args:,args: -- "$@") || return eval "set -- $argv" @@ -59,7 +51,7 @@ parse_cmdline_() { done } -terraform_validate_() { +function terraform_validate_ { # Setup environment variables local var var_name var_value @@ -82,23 +74,23 @@ terraform_validate_() { ((index += 1)) done - local path_uniq - for path_uniq in $(echo "${paths[*]}" | tr ' ' '\n' | sort -u); do - path_uniq="${path_uniq//__REPLACED__SPACE__/ }" + local dir_path + for dir_path in $(echo "${paths[*]}" | tr ' ' '\n' | sort -u); do + dir_path="${dir_path//__REPLACED__SPACE__/ }" - if [[ -n "$(find "$path_uniq" -maxdepth 1 -name '*.tf' -print -quit)" ]]; then + if [[ -n "$(find "$dir_path" -maxdepth 1 -name '*.tf' -print -quit)" ]]; then - pushd "$(realpath "$path_uniq")" > /dev/null + pushd "$(realpath "$dir_path")" > /dev/null - if [[ ! -d .terraform ]]; then + if [ ! -d .terraform ]; then set +e init_output=$(terraform init -backend=false "${INIT_ARGS[@]}" 2>&1) init_code=$? set -e - if [[ $init_code != 0 ]]; then + if [ $init_code -ne 0 ]; then error=1 - echo "Init before validation failed: $path_uniq" + echo "Init before validation failed: $dir_path" echo "$init_output" popd > /dev/null continue @@ -110,9 +102,9 @@ terraform_validate_() { validate_code=$? set -e - if [[ $validate_code != 0 ]]; then + if [ $validate_code -ne 0 ]; then error=1 - echo "Validation failed: $path_uniq" + echo "Validation failed: $dir_path" echo "$validate_output" echo fi @@ -121,7 +113,7 @@ terraform_validate_() { fi done - if [[ $error -ne 0 ]]; then + if [ $error -ne 0 ]; then exit 1 fi } @@ -132,4 +124,4 @@ declare -a INIT_ARGS declare -a ENVS declare -a FILES -[[ ${BASH_SOURCE[0]} != "$0" ]] || main "$@" +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" diff --git a/terragrunt_fmt.sh b/terragrunt_fmt.sh index ee23131..42f9b5e 100755 --- a/terragrunt_fmt.sh +++ b/terragrunt_fmt.sh @@ -1,23 +1,107 @@ #!/usr/bin/env bash +set -eo pipefail -set -e +function main { + common::initialize + common::parse_cmdline "$@" + common::per_dir_hook "${ARGS[*]}" "${FILES[@]}" +} -declare -a paths +function common::initialize { + local SCRIPT_DIR + # get directory containing this script + SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" -index=0 + # source getopt function + # shellcheck source=lib_getopt + . "$SCRIPT_DIR/lib_getopt" +} -for file_with_path in "$@"; do - file_with_path="${file_with_path// /__REPLACED__SPACE__}" +function common::parse_cmdline { + # common global arrays. + # Populated via `common::parse_cmdline` and can be used inside hooks' functions + declare -g -a ARGS=() FILES=() HOOK_CONFIG=() - paths[index]=$(dirname "$file_with_path") + local argv + argv=$(getopt -o a:,h: --long args:,hook-config: -- "$@") || return + eval "set -- $argv" - let "index+=1" -done + for argv; do + case $argv in + -a | --args) + shift + ARGS+=("$1") + shift + ;; + -h | --hook-config) + shift + HOOK_CONFIG+=("$1;") + shift + ;; + --) + shift + FILES=("$@") + break + ;; + esac + done +} -for path_uniq in $(echo "${paths[*]}" | tr ' ' '\n' | sort -u); do - path_uniq="${path_uniq//__REPLACED__SPACE__/ }" +function common::per_dir_hook { + local -r args="$1" + shift 1 + local -a -r files=("$@") - pushd "$path_uniq" > /dev/null - terragrunt hclfmt - popd > /dev/null -done + # 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 + + # 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__/ }" + pushd "$dir_path" > /dev/null || continue + + per_dir_hook_unique_part "$args" "$dir_path" + + local exit_code=$? + if [ $exit_code -ne 0 ]; then + final_exit_code=$exit_code + fi + + popd > /dev/null + 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 +} + +function per_dir_hook_unique_part { + # common logic located in common::per_dir_hook + local -r args="$1" + local -r dir_path="$2" + + # pass the arguments to hook + # shellcheck disable=SC2068 # hook fails when quoting is used ("$arg[@]") + terragrunt hclfmt ${args[@]} + + # return exit code to common::per_dir_hook + local exit_code=$? + return $exit_code +} + +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" diff --git a/terragrunt_validate.sh b/terragrunt_validate.sh index 7f0cf58..1fd83a1 100755 --- a/terragrunt_validate.sh +++ b/terragrunt_validate.sh @@ -1,23 +1,107 @@ #!/usr/bin/env bash +set -eo pipefail -set -e +function main { + common::initialize + common::parse_cmdline "$@" + common::per_dir_hook "${ARGS[*]}" "${FILES[@]}" +} -declare -a paths +function common::initialize { + local SCRIPT_DIR + # get directory containing this script + SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" -index=0 + # source getopt function + # shellcheck source=lib_getopt + . "$SCRIPT_DIR/lib_getopt" +} -for file_with_path in "$@"; do - file_with_path="${file_with_path// /__REPLACED__SPACE__}" +function common::parse_cmdline { + # common global arrays. + # Populated via `common::parse_cmdline` and can be used inside hooks' functions + declare -g -a ARGS=() FILES=() HOOK_CONFIG=() - paths[index]=$(dirname "$file_with_path") + local argv + argv=$(getopt -o a:,h: --long args:,hook-config: -- "$@") || return + eval "set -- $argv" - let "index+=1" -done + for argv; do + case $argv in + -a | --args) + shift + ARGS+=("$1") + shift + ;; + -h | --hook-config) + shift + HOOK_CONFIG+=("$1;") + shift + ;; + --) + shift + FILES=("$@") + break + ;; + esac + done +} -for path_uniq in $(echo "${paths[*]}" | tr ' ' '\n' | sort -u); do - path_uniq="${path_uniq//__REPLACED__SPACE__/ }" +function common::per_dir_hook { + local -r args="$1" + shift 1 + local -a -r files=("$@") - pushd "$path_uniq" > /dev/null - terragrunt validate - popd > /dev/null -done + # 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 + + # 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__/ }" + pushd "$dir_path" > /dev/null || continue + + per_dir_hook_unique_part "$args" "$dir_path" + + local exit_code=$? + if [ $exit_code -ne 0 ]; then + final_exit_code=$exit_code + fi + + popd > /dev/null + 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 +} + +function per_dir_hook_unique_part { + # common logic located in common::per_dir_hook + local -r args="$1" + local -r dir_path="$2" + + # pass the arguments to hook + # shellcheck disable=SC2068 # hook fails when quoting is used ("$arg[@]") + terragrunt validate ${args[@]} + + # return exit code to common::per_dir_hook + local exit_code=$? + return $exit_code +} + +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" diff --git a/terrascan.sh b/terrascan.sh index bd66a73..1ed33bc 100755 --- a/terrascan.sh +++ b/terrascan.sh @@ -1,75 +1,29 @@ #!/usr/bin/env bash set -eo pipefail -main() { - initialize_ - parse_cmdline_ "$@" - terrascan_ "${ARGS[*]}" "${FILES[@]}" +function main { + common::initialize + common::parse_cmdline "$@" + common::per_dir_hook "${ARGS[*]}" "${FILES[@]}" } -terrascan_() { - local -r args="${1}" - shift 1 - local -a -r files=("$@") - - # consume modified files passed from pre-commit so that - # terrascan runs against only those relevant directories - for file_with_path in "${files[@]}"; do - file_with_path="${file_with_path// /__REPLACED__SPACE__}" - paths[index]=$(dirname "$file_with_path") - index=$((index + 1)) - done - - # allow terrascan to continue if exit_code is greater than 0 - # preserve errexit status - shopt -qo errexit && ERREXIT_IS_SET=true - set +e - terrascan_final_exit_code=0 - - # for each path run terrascan - for path_uniq in $(echo "${paths[*]}" | tr ' ' '\n' | sort -u); do - path_uniq="${path_uniq//__REPLACED__SPACE__/ }" - pushd "$path_uniq" > /dev/null - - # pass the arguments to terrascan - # shellcheck disable=SC2086 # terrascan fails when quoting is used ("$arg" vs $arg) - terrascan scan -i terraform $args - - local exit_code=$? - if [ $exit_code != 0 ]; then - terrascan_final_exit_code=$exit_code - fi - - popd > /dev/null - done - - # restore errexit if it was set before the "for" loop - [[ $ERREXIT_IS_SET ]] && set -e - # return the terrascan final exit_code - exit $terrascan_final_exit_code -} - -initialize_() { +function common::initialize { + local SCRIPT_DIR # get directory containing this script - local dir - local source - source="${BASH_SOURCE[0]}" - while [[ -L $source ]]; do # resolve $source until the file is no longer a symlink - dir="$(cd -P "$(dirname "$source")" > /dev/null && pwd)" - source="$(readlink "$source")" - # if $source was a relative symlink, we need to resolve it relative to the path where the symlink file was located - [[ $source != /* ]] && source="$dir/$source" - done - _SCRIPT_DIR="$(dirname "$source")" + SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" # source getopt function # shellcheck source=lib_getopt - . "$_SCRIPT_DIR/lib_getopt" + . "$SCRIPT_DIR/lib_getopt" } -parse_cmdline_() { - declare argv - argv=$(getopt -o a: --long args: -- "$@") || return +function common::parse_cmdline { + # common global arrays. + # Populated via `common::parse_cmdline` and can be used inside hooks' functions + declare -g -a ARGS=() FILES=() HOOK_CONFIG=() + + local argv + argv=$(getopt -o a:,h: --long args:,hook-config: -- "$@") || return eval "set -- $argv" for argv; do @@ -79,17 +33,75 @@ parse_cmdline_() { ARGS+=("$1") shift ;; + -h | --hook-config) + shift + HOOK_CONFIG+=("$1;") + shift + ;; --) shift - FILES+=("$@") + FILES=("$@") break ;; esac done } -# global arrays -declare -a ARGS=() -declare -a FILES=() +function common::per_dir_hook { + local -r args="$1" + shift 1 + local -a -r files=("$@") -[[ ${BASH_SOURCE[0]} != "$0" ]] || main "$@" + # 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 + + # 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__/ }" + pushd "$dir_path" > /dev/null || continue + + per_dir_hook_unique_part "$args" "$dir_path" + + local exit_code=$? + if [ $exit_code -ne 0 ]; then + final_exit_code=$exit_code + fi + + popd > /dev/null + 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 +} + +function per_dir_hook_unique_part { + # common logic located in common::per_dir_hook + local -r args="$1" + local -r dir_path="$2" + + # pass the arguments to hook + # shellcheck disable=SC2068 # hook fails when quoting is used ("$arg[@]") + terrascan scan -i terraform ${args[@]} + + # return exit code to common::per_dir_hook + local exit_code=$? + return $exit_code +} + +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@"