Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/terraform/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "terraform",
"version": "1.5.0",
"version": "1.5.1",
"name": "Terraform, tflint, and TFGrunt",
"documentationURL": "https://github.com/devcontainers/features/tree/main/src/terraform",
"description": "Installs the Terraform CLI and optionally TFLint and Terragrunt. Auto-detects latest version and installs needed dependencies.",
Expand Down
31 changes: 26 additions & 5 deletions src/terraform/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,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)
Expand Down Expand Up @@ -491,7 +512,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
Expand All @@ -506,13 +527,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 \
Expand All @@ -521,12 +542,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
Expand Down
18 changes: 18 additions & 0 deletions test/terraform/scenarios.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
118 changes: 118 additions & 0 deletions test/terraform/tflint_checksum_fallback_helper.sh
Original file line number Diff line number Diff line change
@@ -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" ]
}
34 changes: 34 additions & 0 deletions test/terraform/tflint_checksum_fallback_test.sh
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions test/terraform/tflint_checksum_fallback_test/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FROM mcr.microsoft.com/devcontainers/base:noble
COPY terraform/install.sh /usr/local/share/terraform/install.sh
9 changes: 9 additions & 0 deletions test/terraform/tflint_checksum_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash

set -e

source dev-container-features-test-lib

check "tflint version" tflint --version | grep "0.60.0"

reportResults
Loading