diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b9227841..ede0c63d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,11 +18,14 @@ jobs: strategy: matrix: os: - - 'macos-11' - 'macos-latest' - 'ubuntu-latest' steps: - uses: 'actions/checkout@v2' + with: + fetch-depth: 1 + - name: 'Install Dependencies' + run: './test/install_deps.sh' - name: 'Run all tests' run: './test/run.sh' shell: 'bash' @@ -41,6 +44,10 @@ jobs: - 'windows-2019' steps: - uses: 'actions/checkout@v2' + with: + fetch-depth: 1 + - name: 'Install Dependencies' + run: './test/install_deps.sh' - name: 'Run all tests' run: './test/run.sh' shell: 'bash' diff --git a/README.md b/README.md index 81184913..451d7ac8 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ $ which tfenv Install a specific version of Terraform. -If no parameter is passed, the version to use is resolved automatically via [.terraform-version files](#terraform-version-file) or [TFENV\_TERRAFORM\_VERSION environment variable](#tfenv_terraform_version) (TFENV\_TERRAFORM\_VERSION takes precedence), defaulting to 'latest' if none are found. +If no parameter is passed, the version to use is resolved automatically via [TFENV\_TERRAFORM\_VERSION environment variable](#tfenv_terraform_version), [.terraform-version files](#terraform-version-file), or [required_version in "terraform" section of any .tf or .tf.json file](#min-required), in that order of precedence, i.e. TFENV\_TERRAFORM\_VERSION, then .terraform-version, and then required_version in .tf. The default is 'latest' if none are found. If a parameter is passed, available options: diff --git a/bin/terraform b/bin/terraform index c00b16a8..cec01d26 100755 --- a/bin/terraform +++ b/bin/terraform @@ -26,7 +26,7 @@ if [ -z "${TFENV_ROOT:-""}" ]; then }; TFENV_SHIM=$(readlink_f "${0}") TFENV_ROOT="${TFENV_SHIM%/*/*}"; - [ -n "${TFENV_ROOT}" ] || early_death "Failed to determine TFENV_ROOT" + [ -n "${TFENV_ROOT}" ] || early_death "Failed to determine TFENV_ROOT"; else TFENV_ROOT="${TFENV_ROOT%/}"; fi; @@ -47,7 +47,7 @@ fi; for dir in libexec bin; do case ":${PATH}:" in *:${TFENV_ROOT}/${dir}:*) log 'debug' "\$PATH already contains '${TFENV_ROOT}/${dir}', not adding it again";; - *) + *) log 'debug' "\$PATH does not contain '${TFENV_ROOT}/${dir}', prepending and exporting it now"; export PATH="${TFENV_ROOT}/${dir}:${PATH}"; ;; @@ -62,4 +62,4 @@ log 'debug' "program=\"${0##*/}\""; declare tfenv_path="${TFENV_ROOT}/bin/tfenv"; -tfenv-exec "$@" +tfenv-exec "$@"; diff --git a/bin/tfenv b/bin/tfenv index 0d963bba..28c24a18 100755 --- a/bin/tfenv +++ b/bin/tfenv @@ -26,7 +26,7 @@ if [ -z "${TFENV_ROOT:-""}" ]; then }; TFENV_SHIM=$(readlink_f "${0}") TFENV_ROOT="${TFENV_SHIM%/*/*}"; - [ -n "${TFENV_ROOT}" ] || early_death "Failed to determine TFENV_ROOT" + [ -n "${TFENV_ROOT}" ] || early_death "Failed to determine TFENV_ROOT"; else TFENV_ROOT="${TFENV_ROOT%/}"; @@ -48,7 +48,7 @@ fi; for dir in libexec bin; do case ":${PATH}:" in *:${TFENV_ROOT}/${dir}:*) log 'debug' "\$PATH already contains '${TFENV_ROOT}/${dir}', not adding it again";; - *) + *) log 'debug' "\$PATH does not contain '${TFENV_ROOT}/${dir}', prepending and exporting it now"; export PATH="${TFENV_ROOT}/${dir}:${PATH}"; ;; @@ -62,7 +62,7 @@ done; declare arg="${1:-""}"; log 'debug' "Setting TFENV_DIR to ${PWD}"; -export TFENV_DIR="${PWD}" +export TFENV_DIR="${PWD}"; abort() { log 'debug' 'Aborting...'; diff --git a/lib/bashlog.sh b/lib/bashlog.sh index 4abdaddb..74ed21fa 100755 --- a/lib/bashlog.sh +++ b/lib/bashlog.sh @@ -65,8 +65,8 @@ function log() { local severities_ALERT=1; # Unused local severities_EMERG=0; # Unused - local severity_var="severities_${upper}" - local severity="${!severity_var:-3}" + local severity_var="severities_${upper}"; + local severity="${!severity_var:-3}"; if [ "${debug_level}" -gt 0 ] || [ "${severity}" -lt 7 ]; then @@ -111,7 +111,7 @@ function log() { local colours_DEFAULT='\033[0m' # Default local norm="${colours_DEFAULT}"; - local colour_var="colours_${upper}" + local colour_var="colours_${upper}"; local colour="${!colour_var:-\033[31m}"; local std_line; diff --git a/lib/helpers.sh b/lib/helpers.sh index b06553de..9b623891 100755 --- a/lib/helpers.sh +++ b/lib/helpers.sh @@ -18,7 +18,7 @@ if [ -z "${TFENV_ROOT:-""}" ]; then }; TFENV_SHIM=$(readlink_f "${0}") TFENV_ROOT="${TFENV_SHIM%/*/*}"; - [ -n "${TFENV_ROOT}" ] || early_death "Failed to determine TFENV_ROOT" + [ -n "${TFENV_ROOT}" ] || early_death "Failed to determine TFENV_ROOT"; else TFENV_ROOT="${TFENV_ROOT%/}"; fi; @@ -44,23 +44,24 @@ fi; function load_bashlog () { source "${TFENV_ROOT}/lib/bashlog.sh"; -} +}; export -f load_bashlog; + if [ "${TFENV_DEBUG:-0}" -gt 0 ] ; then # our shim below cannot be used when debugging is enabled - load_bashlog + load_bashlog; else # Shim that understands to no-op for debug messages, and defers to # full bashlog for everything else. function log () { if [ "$1" != 'debug' ] ; then # Loading full bashlog will overwrite the `log` function - load_bashlog - log "$@" - fi - } + load_bashlog; + log "$@"; + fi; + }; export -f log; -fi +fi; resolve_version () { declare version_requested version regex min_required version_file; @@ -75,19 +76,41 @@ resolve_version () { log 'debug' "Version File (${version_file}) is not the default \${TFENV_CONFIG_DIR}/version (${TFENV_CONFIG_DIR}/version)"; version_requested="$(cat "${version_file}")" \ || log 'error' "Failed to open ${version_file}"; + fi - elif [ -f "${version_file}" ]; then + if [ -z "${version_requested:-""}" ]; then + log 'debug' 'Trying to set version from "required_version" under "terraform" section'; + versions="$( echo $(cat {*.tf,*.tf.json} 2>/dev/null | grep -h required_version) | grep -o '\([0-9]\+\.\?\)\{2,3\}\(-[a-z]\+[0-9]\+\)\?')"; + if [[ "${versions}" =~ ([~=!<>]{0,2}[[:blank:]]*[0-9]+[0-9.]+)[^0-9]*(-[a-z]+[0-9]+)? ]]; then + found_min_required="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"; + if [[ "${found_min_required}" =~ ^!=.+ ]]; then + log 'debug' "required_version is a negation - we cannot guess the desired one, skipping."; + else + found_min_required="$(echo "$found_min_required")"; + + # Probably not an advisable way to choose a terraform version, + # but this is the way this functionality works in terraform: + # add .0 to versions without a minor and/or patch version (e.g. 12.0) + while ! [[ "${found_min_required}" =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; do + found_min_required="${found_min_required}.0"; + done; + version_requested="${found_min_required}"; + fi; + fi; + fi; + + if [ -z "${version_requested}" -a -f "${version_file}" ]; then log 'debug' "Version File is the default \${TFENV_CONFIG_DIR}/version (${TFENV_CONFIG_DIR}/version)"; version_requested="$(cat "${version_file}")" \ || log 'error' "Failed to open ${version_file}"; - # Absolute fallback if [ -z "${version_requested}" ]; then log 'debug' 'Version file had no content. Falling back to "latest"'; version_requested='latest'; fi; - else + # Absolute fallback + elif [ -z "${version_requested}" ]; then log 'debug' "Version File is the default \${TFENV_CONFIG_DIR}/version (${TFENV_CONFIG_DIR}/version) but it doesn't exist"; log 'info' 'No version requested on the command line or in the version file search path. Installing "latest"'; version_requested='latest'; @@ -123,7 +146,7 @@ resolve_version () { regex="^${version_requested}$"; log 'debug' "Version is explicit: ${version}. Regex enforces the version: ${regex}"; fi; -} +}; # Curl wrapper to switch TLS option for each OS function curlw () { @@ -141,13 +164,13 @@ function curlw () { fi; curl ${TLS_OPT} ${NETRC_OPT} "$@"; -} +}; export -f curlw; check_active_version() { local v="${1}"; [ -n "$(${TFENV_ROOT}/bin/terraform version | grep -E "^Terraform v${v}((-dev)|( \([a-f0-9]+\)))?$")" ]; -} +}; export -f check_active_version; check_installed_version() { @@ -173,6 +196,8 @@ cleanup() { rm -rf ./.terraform-version; log 'debug' "Deleting ${pwd}/min_required.tf"; rm -rf ./min_required.tf; + log 'debug' "Deleting ${pwd}/required_version.tf"; + rm -rf ./required_version.tf; }; export -f cleanup; @@ -182,6 +207,18 @@ function error_and_proceed() { }; export -f error_and_proceed; +function check_dependencies() { + if [[ $(uname) == 'Darwin' ]] && [ $(which brew) ]; then + if ! [ $(which ggrep) ]; then + log 'error' 'A metaphysical dichotomy has caused this unit to overload and shut down. GNU Grep is a requirement and your Mac does not have it. Consider "brew install grep"'; + fi; + + shopt -s expand_aliases; + alias grep=ggrep; + fi; +}; +export -f check_dependencies; + source "$TFENV_ROOT/lib/tfenv-exec.sh"; source "$TFENV_ROOT/lib/tfenv-version-file.sh"; source "$TFENV_ROOT/lib/tfenv-version-name.sh"; diff --git a/lib/tfenv-exec.sh b/lib/tfenv-exec.sh index d3fe8eb0..a53a115c 100644 --- a/lib/tfenv-exec.sh +++ b/lib/tfenv-exec.sh @@ -33,5 +33,5 @@ function tfenv-exec() { || log 'error' "Failed to execute: ${TF_BIN_PATH} $*"; return 0; -} +}; export -f tfenv-exec; diff --git a/lib/tfenv-version-file.sh b/lib/tfenv-version-file.sh index 78b8de91..8397296c 100644 --- a/lib/tfenv-version-file.sh +++ b/lib/tfenv-version-file.sh @@ -14,7 +14,7 @@ find_local_version_file() { done; log 'debug' "No version file found in ${1}"; return 1; -} +}; export -f find_local_version_file; function tfenv-version-file() { @@ -24,5 +24,5 @@ function tfenv-version-file() { echo "${TFENV_CONFIG_DIR}/version"; fi; fi; -} +}; export -f tfenv-version-file; diff --git a/lib/tfenv-version-name.sh b/lib/tfenv-version-name.sh index 421bce8e..eaa84782 100644 --- a/lib/tfenv-version-name.sh +++ b/lib/tfenv-version-name.sh @@ -4,10 +4,36 @@ function tfenv-version-name() { && log 'debug' "TFENV_VERSION_FILE retrieved from tfenv-version-file: ${TFENV_VERSION_FILE}" \ || log 'error' 'Failed to retrieve TFENV_VERSION_FILE from tfenv-version-file'; - TFENV_VERSION="$(cat "${TFENV_VERSION_FILE}" || true)" \ - && log 'debug' "TFENV_VERSION specified in TFENV_VERSION_FILE: ${TFENV_VERSION}"; + if [ "${TFENV_VERSION_FILE}" = "${TFENV_CONFIG_DIR}/version" ]; then + log 'debug' 'Tryng to set version from "required_version" under "terraform" section'; - TFENV_VERSION_SOURCE="${TFENV_VERSION_FILE}"; + versions="$( echo $(cat {*.tf,*.tf.json} 2>/dev/null | grep -h required_version) | grep -o '\([0-9]\+\.\?\)\{2,3\}\(-[a-z]\+[0-9]\+\)\?')"; + if [[ "${versions}" =~ ([~=!<>]{0,2}[[:blank:]]*[0-9]+[0-9.]+)[^0-9]*(-[a-z]+[0-9]+)? ]]; then + found_min_required="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"; + if [[ "${found_min_required}" =~ ^!=.+ ]]; then + log 'debug' "required_version is a negation - we cannot guess the desired one, skipping."; + else + found_min_required="$(echo "$found_min_required")"; + + # Probably not an advisable way to choose a terraform version, + # but this is the way this functionality works in terraform: + # add .0 to versions without a minor and/or patch version (e.g. 12.0) + while ! [[ "${found_min_required}" =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; do + found_min_required="${found_min_required}.0"; + done; + TFENV_VERSION="${found_min_required}"; + fi; + fi; + + TFENV_VERSION_SOURCE='terraform{required_version}'; + fi; + + if [[ -z "${TFENV_VERSION:-""}" ]]; then + TFENV_VERSION="$(cat "${TFENV_VERSION_FILE}" || true)" \ + && log 'debug' "TFENV_VERSION specified in TFENV_VERSION_FILE: ${TFENV_VERSION}"; + + TFENV_VERSION_SOURCE="${TFENV_VERSION_FILE}"; + fi; else TFENV_VERSION="${TFENV_TERRAFORM_VERSION}" \ && log 'debug' "TFENV_VERSION specified in TFENV_TERRAFORM_VERSION: ${TFENV_VERSION}"; @@ -63,7 +89,7 @@ function tfenv-version-name() { # Accept a v-prefixed version, but strip the v. if [[ "${TFENV_VERSION}" =~ ^v.*$ ]]; then - log 'debug' "Version Requested is prefixed with a v. Stripping the v." + log 'debug' "Version Requested is prefixed with a v. Stripping the v."; TFENV_VERSION="${TFENV_VERSION#v*}"; fi; fi; @@ -77,5 +103,6 @@ function tfenv-version-name() { fi; echo "${TFENV_VERSION}"; -} +}; export -f tfenv-version-name; + diff --git a/libexec/tfenv---version b/libexec/tfenv---version index 2e819ac9..52198f83 100755 --- a/libexec/tfenv---version +++ b/libexec/tfenv---version @@ -57,7 +57,7 @@ fi; for dir in libexec bin; do case ":${PATH}:" in *:${TFENV_ROOT}/${dir}:*) log 'debug' "\$PATH already contains '${TFENV_ROOT}/${dir}', not adding it again";; - *) + *) log 'debug' "\$PATH does not contain '${TFENV_ROOT}/${dir}', prepending and exporting it now"; export PATH="${TFENV_ROOT}/${dir}:${PATH}"; ;; diff --git a/libexec/tfenv-exec b/libexec/tfenv-exec index d3ae7dcf..9d3be482 100755 --- a/libexec/tfenv-exec +++ b/libexec/tfenv-exec @@ -61,7 +61,7 @@ fi; for dir in libexec bin; do case ":${PATH}:" in *:${TFENV_ROOT}/${dir}:*) log 'debug' "\$PATH already contains '${TFENV_ROOT}/${dir}', not adding it again";; - *) + *) log 'debug' "\$PATH does not contain '${TFENV_ROOT}/${dir}', prepending and exporting it now"; export PATH="${TFENV_ROOT}/${dir}:${PATH}"; ;; @@ -72,4 +72,4 @@ done; # Begin Script Body # ##################### -tfenv-exec "$@" +tfenv-exec "$@"; diff --git a/libexec/tfenv-init b/libexec/tfenv-init index 870c14d9..da67a372 100755 --- a/libexec/tfenv-init +++ b/libexec/tfenv-init @@ -1,5 +1,5 @@ #!/usr/bin/env bash -[ -n "${TFENV_DEBUG}" ] && set -x +[ -n "${TFENV_DEBUG}" ] && set -x: -export PATH="${TFENV_ROOT}/bin:${PATH}" +export PATH="${TFENV_ROOT}/bin:${PATH}"; diff --git a/libexec/tfenv-install b/libexec/tfenv-install index a791622e..e22291b3 100755 --- a/libexec/tfenv-install +++ b/libexec/tfenv-install @@ -73,8 +73,8 @@ declare regex="${resolved##*\:}"; log 'debug' "Processing install for version ${version}, using regex ${regex}"; -version="$(tfenv-list-remote | grep -e "${regex}" | head -n 1)"; -[ -n "${version}" ] || log 'error' "No versions matching '${requested}' found in remote"; +remote_version="$(tfenv-list-remote | grep -e "${regex}" | head -n 1)"; +[ -n "${remote_version}" ] && version="${remote_version}" || log 'error' "No versions matching '${requested:-$version}' found in remote"; dst_path="${TFENV_CONFIG_DIR}/versions/${version}"; if [ -f "${dst_path}/terraform" ]; then @@ -118,7 +118,7 @@ case "$(uname -s)" in os="windows_${TFENV_ARCH}"; ;; FreeBSD*) - os="freebsd_${TFENV_ARCH}" + os="freebsd_${TFENV_ARCH}"; ;; *) os="linux_${TFENV_ARCH}"; @@ -140,17 +140,20 @@ else fi; shasums_name="terraform_${version}_SHA256SUMS"; -shasums_signing_key_postfix=".72D7468F" +shasums_signing_key_postfix=".72D7468F"; shasums_sig="${shasums_name}${shasums_signing_key_postfix}.sig"; log 'info' "Installing Terraform v${version}"; # Create a local temporary directory for downloads -tmpdir_arg="-t" +tmpdir_arg="-t"; + if mktemp --help 2>&1 | grep -- '--tmpdir' >/dev/null; then - tmpdir_arg="--tmpdir" -fi + tmpdir_arg="--tmpdir"; +fi; + download_tmp="$(mktemp -d ${tmpdir_arg} tfenv_download.XXXXXX)" || log 'error' "Unable to create temporary download directory in $(pwd)"; + # Clean it up in case of error trap "rm -rf ${download_tmp}" EXIT; @@ -188,13 +191,10 @@ download_signature() { }; # If on MacOS with Homebrew, use GNU grep -# This allows keybase login detection to work on Mac -if [[ $(uname) == 'Darwin' ]] && [ $(which brew) ]; then - if ! [ $(which ggrep) ]; then - brew install grep; - fi - alias grep=ggrep; -fi +# This allows keybase login detection to work on Mac, +# and is required to be able to detect terraform version +# from "required_version" setting in "*.tf" files +check_dependencies; # Verify signature if verification mechanism (keybase, gpg, etc) is present if [[ -f "${TFENV_CONFIG_DIR}/use-gnupg" ]]; then diff --git a/libexec/tfenv-list b/libexec/tfenv-list index ab98d8fd..3e50c51c 100755 --- a/libexec/tfenv-list +++ b/libexec/tfenv-list @@ -48,7 +48,7 @@ fi; for dir in libexec bin; do case ":${PATH}:" in *:${TFENV_ROOT}/${dir}:*) log 'debug' "\$PATH already contains '${TFENV_ROOT}/${dir}', not adding it again";; - *) + *) log 'debug' "\$PATH does not contain '${TFENV_ROOT}/${dir}', prepending and exporting it now"; export PATH="${TFENV_ROOT}/${dir}:${PATH}"; ;; @@ -60,10 +60,10 @@ done; ##################### [ "${#}" -ne 0 ] \ - && log 'error' "usage: tfenv list" + && log 'error' "usage: tfenv list"; [ -d "${TFENV_CONFIG_DIR}/versions" ] \ - || log 'error' 'No versions available. Please install one with: tfenv install' + || log 'error' 'No versions available. Please install one with: tfenv install'; [[ -x "${TFENV_CONFIG_DIR}/versions" && -r "${TFENV_CONFIG_DIR}/versions" ]] \ || log 'error' "tfenv versions directory is inaccessible: ${TFENV_CONFIG_DIR}/versions"; @@ -78,7 +78,7 @@ if [ -z "${TFENV_TERRAFORM_VERSION:-""}" ]; then || log 'error' "tfenv-version-file failed"; else version_source='TFENV_TERRAFORM_VERSION'; -fi +fi; export version_source; # Register for whether a default terraform version has yet been set @@ -87,7 +87,7 @@ declare -i default_set=0; print_version () { if [ "${1}" == "${version_name}" ]; then echo "* ${1} (set by ${version_source})"; - default_set=1 + default_set=1; else echo " ${1}"; fi; diff --git a/libexec/tfenv-list-remote b/libexec/tfenv-list-remote index 97e6d618..cd1bd0f2 100755 --- a/libexec/tfenv-list-remote +++ b/libexec/tfenv-list-remote @@ -48,7 +48,7 @@ fi; for dir in libexec bin; do case ":${PATH}:" in *:${TFENV_ROOT}/${dir}:*) log 'debug' "\$PATH already contains '${TFENV_ROOT}/${dir}', not adding it again";; - *) + *) log 'debug' "\$PATH does not contain '${TFENV_ROOT}/${dir}', prepending and exporting it now"; export PATH="${TFENV_ROOT}/${dir}:${PATH}"; ;; @@ -60,11 +60,11 @@ done; ##################### if [ "${#}" -ne 0 ];then - echo "usage: tfenv list-remote" 1>&2 + echo "usage: tfenv list-remote" 1>&2; exit 1; -fi +fi; -TFENV_REMOTE="${TFENV_REMOTE:-https://releases.hashicorp.com}" +TFENV_REMOTE="${TFENV_REMOTE:-https://releases.hashicorp.com}"; log 'debug' "TFENV_REMOTE: ${TFENV_REMOTE}"; declare remote_versions; diff --git a/libexec/tfenv-min-required b/libexec/tfenv-min-required index 3e4f6b06..36359c1c 100755 --- a/libexec/tfenv-min-required +++ b/libexec/tfenv-min-required @@ -77,7 +77,7 @@ find_min_required() { versions="$( echo $(grep -h required_version "${root}"/{*.tf,*.tf.json} 2>/dev/null ) | grep -o '\([0-9]\+\.\?\)\{2,3\}\(-[a-z]\+[0-9]\+\)\?')"; if [[ "${versions}" =~ ([~=!<>]{0,2}[[:blank:]]*[0-9]+[0-9.]+)[^0-9]*(-[a-z]+[0-9]+)? ]]; then - found_min_required="${BASH_REMATCH[1]}${BASH_REMATCH[2]}" + found_min_required="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"; if [[ "${found_min_required}" =~ ^!=.+ ]]; then log 'debug' "Error: Min required version is a negation ($found_min_required) - we cannot guess the desired one."; diff --git a/libexec/tfenv-pin b/libexec/tfenv-pin index 848c585c..1c7077cf 100755 --- a/libexec/tfenv-pin +++ b/libexec/tfenv-pin @@ -48,7 +48,7 @@ fi; for dir in libexec bin; do case ":${PATH}:" in *:${TFENV_ROOT}/${dir}:*) log 'debug' "\$PATH already contains '${TFENV_ROOT}/${dir}', not adding it again";; - *) + *) log 'debug' "\$PATH does not contain '${TFENV_ROOT}/${dir}', prepending and exporting it now"; export PATH="${TFENV_ROOT}/${dir}:${PATH}"; ;; @@ -60,10 +60,10 @@ done; ##################### [ "${#}" -ne 0 ] \ - && log 'error' "usage: tfenv pin" + && log 'error' "usage: tfenv pin"; [ -d "${TFENV_CONFIG_DIR}/versions/" ] \ - || log 'error' 'No versions available. Please install one with: tfenv install' + || log 'error' 'No versions available. Please install one with: tfenv install'; [[ -x "${TFENV_CONFIG_DIR}/versions" && -r "${TFENV_CONFIG_DIR}/versions" ]] \ || log 'error' "tfenv versions directory is inaccessible: ${TFENV_CONFIG_DIR}/versions"; diff --git a/libexec/tfenv-resolve-version b/libexec/tfenv-resolve-version index 5974aa09..eb671367 100755 --- a/libexec/tfenv-resolve-version +++ b/libexec/tfenv-resolve-version @@ -50,13 +50,19 @@ fi; for dir in libexec bin; do case ":${PATH}:" in *:${TFENV_ROOT}/${dir}:*) log 'debug' "\$PATH already contains '${TFENV_ROOT}/${dir}', not adding it again";; - *) + *) log 'debug' "\$PATH does not contain '${TFENV_ROOT}/${dir}', prepending and exporting it now"; export PATH="${TFENV_ROOT}/${dir}:${PATH}"; ;; esac; done; +# If on MacOS with Homebrew, use GNU grep +# This allows keybase login detection to work on Mac, +# and is required to be able to detect terraform version +# from "required_version" setting in "*.tf" files +check_dependencies; + ##################### # Begin Script Body # ##################### @@ -73,19 +79,41 @@ if [ -z "${arg}" -a -z "${TFENV_TERRAFORM_VERSION:-""}" ]; then log 'debug' "Version File (${version_file}) is not the default \${TFENV_CONFIG_DIR}/version (${TFENV_CONFIG_DIR}/version)"; version_requested="$(cat "${version_file}")" \ || log 'error' "Failed to open ${version_file}"; + fi; + + if [ -z "${version_requested:-""}" ]; then + log 'debug' 'Tryng to set version from "required_version" under "terraform" section'; + versions="$( echo $(cat {*.tf,*.tf.json} 2>/dev/null | grep -h required_version) | grep -o '\([0-9]\+\.\?\)\{2,3\}\(-[a-z]\+[0-9]\+\)\?')"; + if [[ "${versions}" =~ ([~=!<>]{0,2}[[:blank:]]*[0-9]+[0-9.]+)[^0-9]*(-[a-z]+[0-9]+)? ]]; then + found_min_required="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"; + if [[ "${found_min_required}" =~ ^!=.+ ]]; then + log 'debug' "required_version is a negation - we cannot guess the desired one, skipping."; + else + found_min_required="$(echo "$found_min_required")"; + + # Probably not an advisable way to choose a terraform version, + # but this is the way this functionality works in terraform: + # add .0 to versions without a minor and/or patch version (e.g. 12.0) + while ! [[ "${found_min_required}" =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; do + found_min_required="${found_min_required}.0"; + done; + version_requested="${found_min_required}"; + fi; + fi; + fi; - elif [ -f "${version_file}" ]; then + if [ -z "${version_requested}" -a -f "${version_file}" ]; then log 'debug' "Version File is the default \${TFENV_CONFIG_DIR}/version (${TFENV_CONFIG_DIR}/version)"; version_requested="$(cat "${version_file}")" \ || log 'error' "Failed to open ${version_file}"; - # Absolute fallback if [ -z "${version_requested}" ]; then log 'debug' 'Version file had no content. Falling back to "latest"'; version_requested='latest'; fi; - else + # Absolute fallback + elif [ -z "${version_requested}" ]; then log 'debug' "Version File is the default \${TFENV_CONFIG_DIR}/version (${TFENV_CONFIG_DIR}/version) but it doesn't exist"; log 'info' 'No version requested on the command line or in the version file search path. Installing "latest"'; version_requested='latest'; @@ -101,7 +129,7 @@ log 'debug' "Version Requested: ${version_requested}"; # Accept a v-prefixed version, but strip the v. if [[ "${version_requested}" =~ ^v.*$ ]]; then - log 'debug' "Version Requested is prefixed with a v. Stripping the v." + log 'debug' "Version Requested is prefixed with a v. Stripping the v."; version_requested="${version_requested#v*}"; fi; diff --git a/libexec/tfenv-uninstall b/libexec/tfenv-uninstall index 21a61072..eb240e88 100755 --- a/libexec/tfenv-uninstall +++ b/libexec/tfenv-uninstall @@ -48,7 +48,7 @@ fi; for dir in libexec bin; do case ":${PATH}:" in *:${TFENV_ROOT}/${dir}:*) log 'debug' "\$PATH already contains '${TFENV_ROOT}/${dir}', not adding it again";; - *) + *) log 'debug' "\$PATH does not contain '${TFENV_ROOT}/${dir}', prepending and exporting it now"; export PATH="${TFENV_ROOT}/${dir}:${PATH}"; ;; @@ -112,12 +112,12 @@ version="$(tfenv-list | sed -E 's/^(\*| )? //g; s/ \(set by .+\)$//' | grep -e " [ -n "${version}" ] || log 'error' "No versions matching '${regex}' found in local"; dst_path="${TFENV_CONFIG_DIR}/versions/${version}"; -if [ -f "${dst_path}/terraform" ]; then +if [ -f "${dst_path}/terraform" ]; then log 'info' "Uninstall Terraform v${version}"; rm -r "${dst_path}"; - + # If no versions remain, remove the versions directory rmdir "${TFENV_CONFIG_DIR}/versions" 2>/dev/null; - + log 'info' "Terraform v${version} is successfully uninstalled"; fi; diff --git a/libexec/tfenv-use b/libexec/tfenv-use index 304a3c52..7a30ceca 100755 --- a/libexec/tfenv-use +++ b/libexec/tfenv-use @@ -47,7 +47,7 @@ fi; for dir in libexec bin; do case ":${PATH}:" in *:${TFENV_ROOT}/${dir}:*) log 'debug' "\$PATH already contains '${TFENV_ROOT}/${dir}', not adding it again";; - *) + *) log 'debug' "\$PATH does not contain '${TFENV_ROOT}/${dir}', prepending and exporting it now"; export PATH="${TFENV_ROOT}/${dir}:${PATH}"; ;; @@ -73,12 +73,13 @@ if [ -z "${requested_arg}" -a -z "${TFENV_TERRAFORM_VERSION:-""}" ]; then elif [ -z "${requested_arg}" ]; then version_source_suffix=' (set by TFENV_TERRAFORM_VERSION)'; requested="${TFENV_TERRAFORM_VERSION}"; -fi +fi; log debug "Resolving version with: tfenv-resolve-version ${requested_arg}"; declare resolved="$(tfenv-resolve-version ${requested_arg})"; log debug "Resolved to: ${resolved}"; +declare requested_version="${resolved%%\:*}"; declare version="${resolved%%\:*}"; declare regex="${resolved##*\:}"; @@ -92,7 +93,7 @@ declare version="$(\find "${TFENV_CONFIG_DIR}/versions/" -type d -exec basename [ -n "${version}" ] \ && log 'debug' "Found version: ${version}" \ - || log 'error' "No installed versions of terraform matched '${requested}'${version_source_suffix}"; + || log 'error' "No installed versions of terraform matched '${requested_version}'${version_source_suffix}"; target_path="${TFENV_CONFIG_DIR}/versions/${version}"; [ -f "${target_path}/terraform" ] \ diff --git a/libexec/tfenv-version-file b/libexec/tfenv-version-file index 6c2ae3f4..153a29ea 100755 --- a/libexec/tfenv-version-file +++ b/libexec/tfenv-version-file @@ -50,7 +50,7 @@ fi; for dir in libexec bin; do case ":${PATH}:" in *:${TFENV_ROOT}/${dir}:*) log 'debug' "\$PATH already contains '${TFENV_ROOT}/${dir}', not adding it again";; - *) + *) log 'debug' "\$PATH does not contain '${TFENV_ROOT}/${dir}', prepending and exporting it now"; export PATH="${TFENV_ROOT}/${dir}:${PATH}"; ;; @@ -61,4 +61,4 @@ done; # Begin Script Body # ##################### -tfenv-version-file "$@" +tfenv-version-file "$@"; diff --git a/libexec/tfenv-version-name b/libexec/tfenv-version-name index d90884e9..db4ee4c8 100755 --- a/libexec/tfenv-version-name +++ b/libexec/tfenv-version-name @@ -56,8 +56,14 @@ for dir in libexec bin; do esac; done; +# If on MacOS with Homebrew, use GNU grep +# This allows keybase login detection to work on Mac, +# and is required to be able to detect terraform version +# from "required_version" setting in "*.tf" files +check_dependencies; + ##################### # Begin Script Body # ##################### -tfenv-version-name "$@" +tfenv-version-name "$@"; diff --git a/test/install_deps.sh b/test/install_deps.sh new file mode 100755 index 00000000..f1bd2d69 --- /dev/null +++ b/test/install_deps.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -uo pipefail; + +if [[ $(uname) == 'Darwin' ]] && [ $(which brew) ]; then + brew install grep; +fi; diff --git a/test/test_install_and_use_required_version.sh b/test/test_install_and_use_required_version.sh new file mode 100755 index 00000000..b1b5437b --- /dev/null +++ b/test/test_install_and_use_required_version.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash + +set -uo pipefail; + +#################################### +# Ensure we can execute standalone # +#################################### + +function early_death() { + echo "[FATAL] ${0}: ${1}" >&2; + exit 1; +}; + +if [ -z "${TFENV_ROOT:-""}" ]; then + # http://stackoverflow.com/questions/1055671/how-can-i-get-the-behavior-of-gnus-readlink-f-on-a-mac + readlink_f() { + local target_file="${1}"; + local file_name; + + while [ "${target_file}" != "" ]; do + cd "$(dirname ${target_file})" || early_death "Failed to 'cd \$(dirname ${target_file})' while trying to determine TFENV_ROOT"; + file_name="$(basename "${target_file}")" || early_death "Failed to 'basename \"${target_file}\"' while trying to determine TFENV_ROOT"; + target_file="$(readlink "${file_name}")"; + done; + + echo "$(pwd -P)/${file_name}"; + }; + + TFENV_ROOT="$(cd "$(dirname "$(readlink_f "${0}")")/.." && pwd)"; + [ -n ${TFENV_ROOT} ] || early_death "Failed to 'cd \"\$(dirname \"\$(readlink_f \"${0}\")\")/..\" && pwd' while trying to determine TFENV_ROOT"; +else + TFENV_ROOT="${TFENV_ROOT%/}"; +fi; +export TFENV_ROOT; + +if [ -n "${TFENV_HELPERS:-""}" ]; then + log 'debug' 'TFENV_HELPERS is set, not sourcing helpers again'; +else + [ "${TFENV_DEBUG:-0}" -gt 0 ] && echo "[DEBUG] Sourcing helpers from ${TFENV_ROOT}/lib/helpers.sh"; + if source "${TFENV_ROOT}/lib/helpers.sh"; then + log 'debug' 'Helpers sourced successfully'; + else + early_death "Failed to source helpers from ${TFENV_ROOT}/lib/helpers.sh"; + fi; +fi; + +##################### +# Begin Script Body # +##################### + +declare -a errors=(); + +cleanup || log 'error' 'Cleanup failed?!'; + + +log 'info' '### Install required_version normal version (#.#.#)'; + +reqv='0.14.11'; + +echo "terraform { + required_version = \">= ${reqv}\" +}" > required_version.tf; + +( + tfenv install; + tfenv use; + check_active_version "${reqv}"; +) || error_and_proceed 'required_version does not match'; + +cleanup || log 'error' 'Cleanup failed?!'; + + +log 'info' '### Install required_version normal version with arbitrary whitespace (#.#.#)'; + +reqv='0.14.11'; + +echo "terraform { required_version = \" >= ${reqv} \" }" > required_version.tf; + +( + tfenv install; + tfenv use; + check_active_version "${reqv}"; +) || error_and_proceed 'required_version does not match'; + +cleanup || log 'error' 'Cleanup failed?!'; + + +log 'info' '### Install required_version tagged version (#.#.#-tag#)' + +reqv='0.14.0-rc1' + +echo "terraform { + required_version = \">=${reqv}\" +}" > required_version.tf; + +( + tfenv install; + tfenv use; + check_active_version "${reqv}"; +) || error_and_proceed 'required_version tagged-version does not match'; + +cleanup || log 'error' 'Cleanup failed?!'; + + +log 'info' '### Install required_version incomplete version (#.#.)' + +reqv='0.14'; + +echo "terraform { + required_version = \">=${reqv}\" +}" > required_version.tf; + +( + tfenv install; + tfenv use; + check_active_version "${reqv}.0"; +) || error_and_proceed 'required_version incomplete-version does not match'; + +cleanup || log 'error' 'Cleanup failed?!'; + +log 'info' '### Test auto-installing when running terraform'; + +reqv='0.14.11'; + +echo "terraform { + required_version = \">=${reqv}\" +}" > required_version.tf; + +( + check_active_version "${reqv}"; +) || error_and_proceed 'required_version does not match'; + +cleanup || log 'error' 'Cleanup failed?!'; + +if [ "${#errors[@]}" -gt 0 ]; then + log 'warn' '===== The following required_version tests failed ====='; + for error in "${errors[@]}"; do + log 'warn' "\t${error}"; + done; + log 'error' 'required_version test failure(s)'; +else + log 'info' 'All required_version tests passed.'; +fi; + +exit 0;