diff --git a/src/terraform/install.sh b/src/terraform/install.sh index 68cff24b9..cf0dab612 100755 --- a/src/terraform/install.sh +++ b/src/terraform/install.sh @@ -469,6 +469,27 @@ install_tflint() { curl -sSL -o /tmp/tf-downloads/${TFLINT_FILENAME} https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/${TFLINT_FILENAME} } +download_tflint_checksums() { + curl -fsSL -o /tmp/tf-downloads/tflint_checksums.txt "https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt" || return 1 + if [ ! -s "/tmp/tf-downloads/${TFLINT_FILENAME}" ] || grep -q "Not Found" "/tmp/tf-downloads/${TFLINT_FILENAME}"; then + return 1 + fi + awk -v filename="${TFLINT_FILENAME}" '$2 == filename || $2 == "*" filename { found = 1 } END { exit !found }' /tmp/tf-downloads/tflint_checksums.txt +} + +install_tflint_with_checksum_fallback() { + while ! download_tflint_checksums; do + current_tflint_version="${TFLINT_VERSION}" + echo "(!) Failed to fetch checksums for tflint v${current_tflint_version}, trying the previous release." + find_prev_version_from_git_tags TFLINT_VERSION "$tflint_url" + if [ "${TFLINT_VERSION}" = "${current_tflint_version}" ]; then + echo "(!) Unable to find a tflint release with downloadable checksums." + exit 1 + fi + install_tflint "$TFLINT_VERSION" + done +} + verify_tflint_attestations() { local checksums=$1 local checksums_sha256=$(sha256sum "$checksums" | cut -d " " -f 1) @@ -499,7 +520,7 @@ if [ "${TFLINT_VERSION}" != "none" ]; then echo "${TFLINT_SHA256} *${TFLINT_FILENAME}" > tflint_checksums.txt sha256sum --ignore-missing -c tflint_checksums.txt else - curl -sSL -o tflint_checksums.txt https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt + install_tflint_with_checksum_fallback # Attempt GitHub Attestation verification (0.51.1+) set +e @@ -514,13 +535,13 @@ if [ "${TFLINT_VERSION}" != "none" ]; then # Fallback to cosign verification echo "(*) GitHub Attestation verification failed or not supported for this version, falling back to Cosign verification..." set +e - curl -sSL -o checksums.txt.keyless.sig https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt.keyless.sig + curl -fsSL -o checksums.txt.keyless.sig https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt.keyless.sig set -e # Check that checksums.txt.keyless.sig exists and is not empty if [ -s checksums.txt.keyless.sig ]; then # Validate checksums with cosign - curl -sSL -o checksums.txt.pem https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt.pem + curl -fsSL -o checksums.txt.pem https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt.pem ensure_cosign cosign verify-blob \ --certificate=/tmp/tf-downloads/checksums.txt.pem \ @@ -529,12 +550,12 @@ if [ "${TFLINT_VERSION}" != "none" ]; then --certificate-oidc-issuer=https://token.actions.githubusercontent.com \ /tmp/tf-downloads/tflint_checksums.txt # Ensure that checksums.txt has $TFLINT_FILENAME - grep ${TFLINT_FILENAME} /tmp/tf-downloads/tflint_checksums.txt + grep -F "${TFLINT_FILENAME}" /tmp/tf-downloads/tflint_checksums.txt # Validate downloaded file sha256sum --ignore-missing -c tflint_checksums.txt else # Fallback to older, GPG-based verification (pre-0.47.0 of tflint) - curl -sSL -o tflint_checksums.txt.sig https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt.sig + curl -fsSL -o tflint_checksums.txt.sig https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt.sig curl -sSL -o tflint_key "${TFLINT_GPG_KEY_URI}" gpg -q --import tflint_key gpg --verify tflint_checksums.txt.sig tflint_checksums.txt diff --git a/test/terraform/scenarios.json b/test/terraform/scenarios.json index de897b5e9..1bef8f15b 100644 --- a/test/terraform/scenarios.json +++ b/test/terraform/scenarios.json @@ -120,6 +120,24 @@ } } }, + "tflint_checksum_test": { + "image": "mcr.microsoft.com/devcontainers/base:jammy", + "features": { + "terraform": { + "tflint": "0.60.0" + } + } + }, + "tflint_checksum_fallback_test": { + "build": { + "dockerfile": "Dockerfile" + }, + "features": { + "terraform": { + "tflint": "0.60.0" + } + } + }, "older_tflint": { "image": "mcr.microsoft.com/devcontainers/base:jammy", "features": { diff --git a/test/terraform/tflint_checksum_fallback_helper.sh b/test/terraform/tflint_checksum_fallback_helper.sh new file mode 100644 index 000000000..5b2d792ff --- /dev/null +++ b/test/terraform/tflint_checksum_fallback_helper.sh @@ -0,0 +1,118 @@ +#!/bin/bash + +prepare_tflint_fixtures() { + local version + case "$(uname -m)" in + x86_64) architecture=amd64 ;; + aarch64) architecture=arm64 ;; + *) return 1 ;; + esac + TFLINT_FILENAME="tflint_linux_${architecture}.zip" + mkdir -p "${test_root}/fixtures" + for version in 0.60.0 0.59.1 0.58.1; do + mkdir -p "${test_root}/fixtures/${version}" + command curl -fsSL --retry 3 -o "${test_root}/fixtures/${version}/${TFLINT_FILENAME}" \ + "https://github.com/terraform-linters/tflint/releases/download/v${version}/${TFLINT_FILENAME}" || return 1 + command curl -fsSL --retry 3 -o "${test_root}/fixtures/${version}/checksums.txt" \ + "https://github.com/terraform-linters/tflint/releases/download/v${version}/checksums.txt" || return 1 + (cd "${test_root}/fixtures/${version}" && sha256sum --ignore-missing -c checksums.txt) || return 1 + done +} + +run_tflint_case() { + bash -e -c 'source "$1"; installer=$2; test_root=$3; architecture=$4; TFLINT_FILENAME=$5; execute_tflint_case "$6"' \ + bash "$(realpath "${BASH_SOURCE[0]}")" "${installer}" "${test_root}" "${architecture}" "${TFLINT_FILENAME}" "$1" +} + +execute_tflint_case() ( + set -e + local mode=$1 + local case_dir="${test_root}/${mode}" + mkdir -p "${case_dir}/downloads" "${case_dir}/bin" + cd "${case_dir}/downloads" + TFLINT_VERSION=0.60.0 + TFLINT_SHA256=automatic + tflint_url=https://github.com/terraform-linters/tflint + + curl() { + local destination url version asset + while [ "$#" -gt 0 ]; do + case "$1" in + -o) destination=$2; shift 2 ;; + https://*) url=$1; shift ;; + *) shift ;; + esac + done + version=${url%/*} + version=${version##*/v} + asset=${url##*/} + destination=${destination/\/tmp\/tf-downloads/${case_dir}/downloads} + printf '%s/%s\n' "${version}" "${asset}" >> "${case_dir}/requests" + if [ "${asset}" = checksums.txt ]; then + case "${mode}:${version}" in + missing:0.60.0|multiple:0.60.0|multiple:0.59.1|stalled:*) return 22 ;; + empty:0.60.0) : > "${destination}"; return 0 ;; + absent-entry:0.60.0) printf '%064d other.zip\n' 0 > "${destination}"; return 0 ;; + esac + else + case "${mode}:${version}" in + empty-archive:0.60.0) : > "${destination}"; return 0 ;; + missing-archive:0.60.0) printf 'Not Found' > "${destination}"; return 0 ;; + esac + fi + cp "${test_root}/fixtures/${version}/${asset}" "${destination}" + if [ "${mode}" = corrupt ] && [ "${asset}" = "${TFLINT_FILENAME}" ]; then + printf 'corrupted' >> "${destination}" + fi + } + + find_prev_version_from_git_tags() { + printf '%s\n' "${TFLINT_VERSION}" >> "${case_dir}/fallbacks" + [ "${mode}" != stalled ] || return 0 + case "${TFLINT_VERSION}" in + 0.60.0) TFLINT_VERSION=0.59.1 ;; + 0.59.1) TFLINT_VERSION=0.58.1 ;; + esac + } + + install_previous_version() { + find_prev_version_from_git_tags "$1" "$2" + "$3" "${TFLINT_VERSION}" + } + + verify_tflint_attestations() { + [ -s "$1" ] + } + + unzip() { + command unzip "${1/\/tmp\/tf-downloads/${case_dir}/downloads}" + } + + mv() { + [ "$1" = -f ] && [ "$2" = tflint ] && [ "$3" = /usr/local/bin/ ] + command mv -f "$2" "${case_dir}/bin/" + } + + source <(sed -n '/^install_tflint() {/,/^verify_tflint_attestations() {/p' "${installer}" | \ + sed '$d;s|/tmp/tf-downloads|'"${case_dir}"'/downloads|g') + source <(sed -n '/^if \[ "${TFLINT_VERSION}" != "none" \]; then/,/^install_terragrunt() {/p' "${installer}" | \ + sed '$d;s|/tmp/tf-downloads|'"${case_dir}"'/downloads|g') + printf '%s\n' "${TFLINT_VERSION}" > "${case_dir}/selected-version" +) + +check_tflint_installation() { + local mode=$1 version=$2 + run_tflint_case "${mode}" || return 1 + [ "$(cat "${test_root}/${mode}/selected-version")" = "${version}" ] || return 1 + "${test_root}/${mode}/bin/tflint" --version | grep -F "TFLint version ${version}" || return 1 + cmp "${test_root}/fixtures/${version}/${TFLINT_FILENAME}" "${test_root}/${mode}/downloads/${TFLINT_FILENAME}" || return 1 + cmp "${test_root}/fixtures/${version}/checksums.txt" "${test_root}/${mode}/downloads/tflint_checksums.txt" +} + +check_tflint_rejected() { + local mode=$1 + if run_tflint_case "${mode}" > "${test_root}/${mode}.log" 2>&1; then + return 1 + fi + [ ! -e "${test_root}/${mode}/bin/tflint" ] +} \ No newline at end of file diff --git a/test/terraform/tflint_checksum_fallback_test.sh b/test/terraform/tflint_checksum_fallback_test.sh new file mode 100644 index 000000000..85fa76412 --- /dev/null +++ b/test/terraform/tflint_checksum_fallback_test.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -e + +source dev-container-features-test-lib + +source "$(dirname "${BASH_SOURCE[0]}")/tflint_checksum_fallback_helper.sh" + +installer="${TFLINT_TEST_INSTALLER:-/usr/local/share/terraform/install.sh}" +test_root=$(mktemp -d) +trap 'rm -rf "${test_root}"' EXIT +prepare_tflint_fixtures + +check "available checksums install the requested release" check_tflint_installation success 0.60.0 +check "missing checksums install the previous release" check_tflint_installation missing 0.59.1 +check "empty checksums trigger fallback" check_tflint_installation empty 0.59.1 +check "checksums without the archive entry trigger fallback" check_tflint_installation absent-entry 0.59.1 +check "empty archive triggers fallback" check_tflint_installation empty-archive 0.59.1 +check "Not Found archive triggers fallback" check_tflint_installation missing-archive 0.59.1 +check "multiple unavailable releases install matching archive and checksums" check_tflint_installation multiple 0.58.1 +check "each fallback downloads the replacement archive before its checksums" \ + diff -u <(printf '%s\n' \ + "0.60.0/${TFLINT_FILENAME}" 0.60.0/checksums.txt \ + "0.59.1/${TFLINT_FILENAME}" 0.59.1/checksums.txt \ + "0.58.1/${TFLINT_FILENAME}" 0.58.1/checksums.txt) "${test_root}/multiple/requests" +check "checksum mismatch prevents installation" check_tflint_rejected corrupt +check "checksum mismatch is reported" grep -F 'FAILED' "${test_root}/corrupt.log" +check "unchanged previous version fails without installing" check_tflint_rejected stalled +check "unchanged previous version stops after one attempt" \ + test "$(wc -l < "${test_root}/stalled/fallbacks")" -eq 1 +check "exhausted fallback reports a useful error" \ + grep -F 'Unable to find a tflint release with downloadable checksums.' "${test_root}/stalled.log" + +reportResults diff --git a/test/terraform/tflint_checksum_fallback_test/Dockerfile b/test/terraform/tflint_checksum_fallback_test/Dockerfile new file mode 100644 index 000000000..ce324efa1 --- /dev/null +++ b/test/terraform/tflint_checksum_fallback_test/Dockerfile @@ -0,0 +1,2 @@ +FROM mcr.microsoft.com/devcontainers/base:noble +COPY terraform/install.sh /usr/local/share/terraform/install.sh \ No newline at end of file diff --git a/test/terraform/tflint_checksum_test.sh b/test/terraform/tflint_checksum_test.sh new file mode 100644 index 000000000..c1ae1d418 --- /dev/null +++ b/test/terraform/tflint_checksum_test.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e + +source dev-container-features-test-lib + +check "tflint version" tflint --version | grep "0.60.0" + +reportResults