From 535f8bc78db0acf3a8d463898c6f1e1968ad8f61 Mon Sep 17 00:00:00 2001 From: Oliver Ford Date: Fri, 12 Nov 2021 18:20:27 +0000 Subject: [PATCH 1/8] Add `latest-allowed` option from required_version This commit adds an option similar to `min-required` in that it parses the terraform's `required_version`, but then uses the most recent version allowed by that spec. For example, given: required_version = "~> 0.10.0" `min-required` would give us `0.10.0`, while `latest-allowed` will give us `0.10.8` (via `latest:^0.10`). The `<` operator is not implemented (only `>` `>=` `~>` `<=`) because it's messy to find the latest version smaller than a given one in `list-remote`'s output, given it may not (yet) exist, and it seems a reasonably assumption/requirement that the version numbers will exist, since, for example: required_version = "~> 1.0" expresses the same as: required_version = "< 2.0.0" without requiring a version that doesn't (at time of writing) exist. This assumption/requirement stands for `<=` too, but again I think that's reasonable since `<= 2.0.0` (as opposed to not equal to) seems like a strange version constraint to use before it exists. (And when it does, one can just use `2.0.0`.) Closes #307. --- README.md | 13 +++++++++---- lib/tfenv-version-name.sh | 5 +++++ libexec/tfenv-resolve-version | 22 ++++++++++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b413ff6..abba8c2 100644 --- a/README.md +++ b/README.md @@ -88,13 +88,17 @@ If a parameter is passed, available options: - `x.y.z` [Semver 2.0.0](https://semver.org/) string specifying the exact version to install - `latest` is a syntax to install latest version - `latest:` is a syntax to install latest version matching regex (used by grep -e) -- `min-required` is a syntax to recursively scan your Terraform files to detect which version is minimally required. See [required_version](https://www.terraform.io/docs/configuration/terraform.html) docs. Also [see min-required](#min-required) section below. +- `latest-allowed` is a syntax to recursively scan your Terraform files to detect which version is maximally allowed. +- `min-required` is a syntax to recursively scan your Terraform files to detect which version is minimally required. + +See [required_version](https://www.terraform.io/docs/configuration/terraform.html) docs. Also [see min-required & latest-allowed](#min-required) section below. ```console $ tfenv install $ tfenv install 0.7.0 $ tfenv install latest $ tfenv install latest:^0.8 +$ tfenv install latest-allowed $ tfenv install min-required ``` @@ -121,7 +125,8 @@ validation failure. If you use a [.terraform-version](#terraform-version-file) file, `tfenv install` (no argument) will install the version written in it. -#### min-required + +#### min-required & latest-allowed Please note that we don't do semantic version range parsing but use first ever found version as the candidate for minimally required one. It is up to the user to keep the definition reasonable. I.e. @@ -133,9 +138,9 @@ terraform { ``` ```terraform -// this will detect 0.10.0 +// this will detect 0.10.8 (the latest 0.10.x release) terraform { - required_version = ">= 0.10.0, <0.12.3" + required_version = "~> 0.10.0, <0.12.3" } ``` diff --git a/lib/tfenv-version-name.sh b/lib/tfenv-version-name.sh index 6607deb..3975769 100644 --- a/lib/tfenv-version-name.sh +++ b/lib/tfenv-version-name.sh @@ -40,6 +40,11 @@ function tfenv-version-name() { if [[ "${TFENV_VERSION}" =~ ^latest.*$ ]]; then log 'debug' "TFENV_VERSION uses 'latest' keyword: ${TFENV_VERSION}"; + if [[ "${TFENV_VERSION}" == latest-allowed ]]; then + TFENV_VERSION="$(tfenv-resolve-version)"; + log 'debug' "Resolved latest-allowed to: ${TFENV_VERSION}"; + fi; + if [[ "${TFENV_VERSION}" =~ ^latest\:.*$ ]]; then regex="${TFENV_VERSION##*\:}"; log 'debug' "'latest' keyword uses regex: ${regex}"; diff --git a/libexec/tfenv-resolve-version b/libexec/tfenv-resolve-version index ce5cfb0..e4690cd 100755 --- a/libexec/tfenv-resolve-version +++ b/libexec/tfenv-resolve-version @@ -127,6 +127,28 @@ if [[ "${version_requested}" =~ ^min-required$ ]]; then version_requested="${min_required}"; fi; +if [[ "${version_requested}" =~ ^latest-allowed$ ]]; then + log 'info' 'Detecting latest allowable version...'; + version_spec="$(grep -h required_version "${TFENV_DIR:-$(pwd)}"/{*.tf,*.tf.json} 2>/dev/null | rev | cut -d'"' -f2 | rev | cut -d, -f1)" + version_num="$(echo "$version_spec" | sed -E 's/[^0-9.]+//')" + + case "$version_spec" in + '>'*) + version_requested=latest + ;; + '<='*) + version_requested="$version_num" + ;; + '~>'*) + version_without_rightmost="$(echo "$version_num" | rev | cut -d. -f2- | rev)" + version_requested="latest:^${version_without_rightmost}" + ;; + *) + log 'error' "Unsupported version spec: '${version_spec}', only >, >=, <=, and ~> are supported." + ;; + esac +fi; + if [[ "${version_requested}" =~ ^latest\:.*$ ]]; then version="${version_requested%%\:*}"; regex="${version_requested##*\:}"; From 830c2ec2c9ba514d3c621d363fdd8ab23d25be78 Mon Sep 17 00:00:00 2001 From: Oliver Ford Date: Fri, 15 Jul 2022 15:53:58 +0100 Subject: [PATCH 2/8] Remove confusing reference to recursive tf scan --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index abba8c2..9155dbe 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,8 @@ If a parameter is passed, available options: - `x.y.z` [Semver 2.0.0](https://semver.org/) string specifying the exact version to install - `latest` is a syntax to install latest version - `latest:` is a syntax to install latest version matching regex (used by grep -e) -- `latest-allowed` is a syntax to recursively scan your Terraform files to detect which version is maximally allowed. -- `min-required` is a syntax to recursively scan your Terraform files to detect which version is minimally required. +- `latest-allowed` is a syntax to scan your Terraform files to detect which version is maximally allowed. +- `min-required` is a syntax to scan your Terraform files to detect which version is minimally required. See [required_version](https://www.terraform.io/docs/configuration/terraform.html) docs. Also [see min-required & latest-allowed](#min-required) section below. From 585877a364d237d63169c1aabded86ded3450749 Mon Sep 17 00:00:00 2001 From: Oliver Ford Date: Fri, 15 Jul 2022 16:34:56 +0100 Subject: [PATCH 3/8] Match project style --- libexec/tfenv-resolve-version | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/libexec/tfenv-resolve-version b/libexec/tfenv-resolve-version index e4690cd..6e7ad35 100755 --- a/libexec/tfenv-resolve-version +++ b/libexec/tfenv-resolve-version @@ -129,24 +129,24 @@ fi; if [[ "${version_requested}" =~ ^latest-allowed$ ]]; then log 'info' 'Detecting latest allowable version...'; - version_spec="$(grep -h required_version "${TFENV_DIR:-$(pwd)}"/{*.tf,*.tf.json} 2>/dev/null | rev | cut -d'"' -f2 | rev | cut -d, -f1)" - version_num="$(echo "$version_spec" | sed -E 's/[^0-9.]+//')" + version_spec="$(grep -h required_version "${TFENV_DIR:-$(pwd)}"/{*.tf,*.tf.json} 2>/dev/null | rev | cut -d'"' -f2 | rev | cut -d, -f1)"; + version_num="$(echo "${version_spec}" | sed -E 's/[^0-9.]+//')"; - case "$version_spec" in + case "${version_spec}" in '>'*) - version_requested=latest + version_requested=latest; ;; '<='*) - version_requested="$version_num" + version_requested="${version_num}"; ;; '~>'*) - version_without_rightmost="$(echo "$version_num" | rev | cut -d. -f2- | rev)" - version_requested="latest:^${version_without_rightmost}" + version_without_rightmost="$(echo "${version_num}" | rev | cut -d. -f2- | rev)"; + version_requested="latest:^${version_without_rightmost}"; ;; *) - log 'error' "Unsupported version spec: '${version_spec}', only >, >=, <=, and ~> are supported." + log 'error' "Unsupported version spec: '${version_spec}', only >, >=, <=, and ~> are supported."; ;; - esac + esac; fi; if [[ "${version_requested}" =~ ^latest\:.*$ ]]; then From c65aa4fd8e01e159bd8b9cf82eb21505108e90d0 Mon Sep 17 00:00:00 2001 From: Oliver Ford Date: Fri, 15 Jul 2022 16:35:21 +0100 Subject: [PATCH 4/8] Add test of latest-allowed Largely copying that for min-required, mutatis mutandis. --- lib/helpers.sh | 4 +- test/test_use_latestallowed.sh | 141 +++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 1 deletion(-) create mode 100755 test/test_use_latestallowed.sh diff --git a/lib/helpers.sh b/lib/helpers.sh index 69c9b5d..535232d 100755 --- a/lib/helpers.sh +++ b/lib/helpers.sh @@ -91,7 +91,7 @@ function check_active_version() { local active_version="$(${TFENV_ROOT}/bin/terraform ${maybe_chdir} version | grep '^Terraform')"; - if ! grep -E "^Terraform v${v}((-dev)|( \([a-f0-9]+\)))?\$" <(echo "${active_version}"); then + if ! grep -E "^Terraform v${v}((-dev)|( \([a-f0-9]+\)))?( is already installed)?\$" <(echo "${active_version}"); then log 'debug' "Expected version ${v} but found ${active_version}"; return 1; fi; @@ -124,6 +124,8 @@ function cleanup() { rm -rf ./versions; log 'debug' "Deleting ${pwd}/.terraform-version"; rm -rf ./.terraform-version; + log 'debug' "Deleting ${pwd}/latest_allowed.tf"; + rm -rf ./latest_allowed.tf; log 'debug' "Deleting ${pwd}/min_required.tf"; rm -rf ./min_required.tf; log 'debug' "Deleting ${pwd}/chdir-dir"; diff --git a/test/test_use_latestallowed.sh b/test/test_use_latestallowed.sh new file mode 100755 index 0000000..10af0d5 --- /dev/null +++ b/test/test_use_latestallowed.sh @@ -0,0 +1,141 @@ +#!/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 latest-allowed normal version (#.#.#)'; + +echo "terraform { + required_version = \"~> 1.1.0\" +}" > latest_allowed.tf; + +( + tfenv install latest-allowed; + tfenv use latest-allowed; + check_active_version 1.1.9; +) || error_and_proceed 'Latest allowed version does not match'; + +cleanup || log 'error' 'Cleanup failed?!'; + + +log 'info' '### Install latest-allowed tagged version (#.#.#-tag#)' + +echo "terraform { + required_version = \"<=0.13.0-rc1\" +}" > latest_allowed.tf; + +( + tfenv install latest-allowed; + tfenv use latest-allowed; + check_active_version 0.13.0-rc1; +) || error_and_proceed 'Latest allowed tagged-version does not match'; + +cleanup || log 'error' 'Cleanup failed?!'; + + +log 'info' '### Install latest-allowed incomplete version (#.#.)' + +echo "terraform { + required_version = \"~> 0.12\" +}" >> latest_allowed.tf; + +( + tfenv install latest-allowed; + tfenv use latest-allowed; + check_active_version 0.15.5; +) || error_and_proceed 'Latest allowed incomplete-version does not match'; + +cleanup || log 'error' 'Cleanup failed?!'; + + +log 'info' '### Install latest-allowed with TFENV_AUTO_INSTALL'; + +echo "terraform { + required_version = \"~> 1.0.0\" +}" >> latest_allowed.tf; +echo 'latest-allowed' > .terraform-version; + +( + TFENV_AUTO_INSTALL=true terraform version; + check_active_version 1.0.11; +) || error_and_proceed 'Latest allowed auto-installed version does not match'; + +cleanup || log 'error' 'Cleanup failed?!'; + + +log 'info' '### Install latest-allowed with TFENV_AUTO_INSTALL & -chdir'; + +mkdir -p chdir-dir +echo "terraform { + required_version = \"~> 0.14.3\" +}" >> chdir-dir/latest_allowed.tf; +echo 'latest-allowed' > chdir-dir/.terraform-version + +( + TFENV_AUTO_INSTALL=true terraform -chdir=chdir-dir version; + check_active_version 0.14.11 chdir-dir; +) || error_and_proceed 'Latest allowed version from -chdir does not match'; + +cleanup || log 'error' 'Cleanup failed?!'; + +if [ "${#errors[@]}" -gt 0 ]; then + log 'warn' '===== The following use_latestallowed tests failed ====='; + for error in "${errors[@]}"; do + log 'warn' "\t${error}"; + done; + log 'error' 'use_latestallowed test failure(s)'; +else + log 'info' 'All use_latestallowed tests passed.'; +fi; + +exit 0; From 42b54562c5d552ddceaf630b98d145296f1b375c Mon Sep 17 00:00:00 2001 From: Oliver Ford Date: Fri, 15 Jul 2022 16:41:31 +0100 Subject: [PATCH 5/8] Fix latest-allowed for partial versions --- libexec/tfenv-resolve-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libexec/tfenv-resolve-version b/libexec/tfenv-resolve-version index 6e7ad35..48af065 100755 --- a/libexec/tfenv-resolve-version +++ b/libexec/tfenv-resolve-version @@ -140,7 +140,7 @@ if [[ "${version_requested}" =~ ^latest-allowed$ ]]; then version_requested="${version_num}"; ;; '~>'*) - version_without_rightmost="$(echo "${version_num}" | rev | cut -d. -f2- | rev)"; + version_without_rightmost="$(echo "${version_num}" | sed 's/\./\n./g' | head -n -1 | xargs -I@ echo -n @)"; version_requested="latest:^${version_without_rightmost}"; ;; *) From cd77bbbb8e3610675d361eb70fe77b553cdc3d4d Mon Sep 17 00:00:00 2001 From: Oliver Ford Date: Fri, 15 Jul 2022 16:43:53 +0100 Subject: [PATCH 6/8] Add debug logging --- libexec/tfenv-resolve-version | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libexec/tfenv-resolve-version b/libexec/tfenv-resolve-version index 48af065..d9ab6ae 100755 --- a/libexec/tfenv-resolve-version +++ b/libexec/tfenv-resolve-version @@ -128,9 +128,10 @@ if [[ "${version_requested}" =~ ^min-required$ ]]; then fi; if [[ "${version_requested}" =~ ^latest-allowed$ ]]; then - log 'info' 'Detecting latest allowable version...'; + log 'debug' 'Detecting latest allowable version...'; version_spec="$(grep -h required_version "${TFENV_DIR:-$(pwd)}"/{*.tf,*.tf.json} 2>/dev/null | rev | cut -d'"' -f2 | rev | cut -d, -f1)"; version_num="$(echo "${version_spec}" | sed -E 's/[^0-9.]+//')"; + log 'debug' "Using ${version_num} from version spec: ${version_spec}"; case "${version_spec}" in '>'*) @@ -147,6 +148,7 @@ if [[ "${version_requested}" =~ ^latest-allowed$ ]]; then log 'error' "Unsupported version spec: '${version_spec}', only >, >=, <=, and ~> are supported."; ;; esac; + log 'debug' "Determined the requested version to be: ${version_requested}"; fi; if [[ "${version_requested}" =~ ^latest\:.*$ ]]; then From c5689978928ff8366e82c04f649d6da2896d5c35 Mon Sep 17 00:00:00 2001 From: Oliver Ford Date: Fri, 15 Jul 2022 17:19:54 +0100 Subject: [PATCH 7/8] Error better using uninstall with latest-allowed --- libexec/tfenv-uninstall | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libexec/tfenv-uninstall b/libexec/tfenv-uninstall index eb240e8..83289c8 100755 --- a/libexec/tfenv-uninstall +++ b/libexec/tfenv-uninstall @@ -93,6 +93,10 @@ if [[ "${version_requested}" =~ ^min-required$ ]]; then log 'error' 'min-required is an unsupported option for uninstall'; fi; +if [[ "${version_requested}" == latest-allowed ]]; then + log 'error' 'latest-allowed is an unsupported option for uninstall'; +fi; + if [[ "${version_requested}" =~ ^latest\:.*$ ]]; then version="${version_requested%%\:*}"; regex="${version_requested##*\:}"; From 699f998f83ca76bb9ae5971cd34133a00cdf28d5 Mon Sep 17 00:00:00 2001 From: Oliver Ford Date: Fri, 15 Jul 2022 21:17:34 +0100 Subject: [PATCH 8/8] Fix invalid argument to head on macOS --- libexec/tfenv-resolve-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libexec/tfenv-resolve-version b/libexec/tfenv-resolve-version index d9ab6ae..d0b66bc 100755 --- a/libexec/tfenv-resolve-version +++ b/libexec/tfenv-resolve-version @@ -141,7 +141,7 @@ if [[ "${version_requested}" =~ ^latest-allowed$ ]]; then version_requested="${version_num}"; ;; '~>'*) - version_without_rightmost="$(echo "${version_num}" | sed 's/\./\n./g' | head -n -1 | xargs -I@ echo -n @)"; + version_without_rightmost="$(echo "${version_num}" | rev | cut -d. -f2- | rev)"; version_requested="latest:^${version_without_rightmost}"; ;; *)