sless-primer/run_terraform_examples.sh

333 lines
9.9 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_DIR="$ROOT_DIR/.test-logs"
mkdir -p "$LOG_DIR"
EXAMPLES=(
"hello-node"
"simple-node"
"simple-python"
"notes-python"
)
declare -A CHECK_METHOD=(
[hello-node]="POST"
[simple-node]="GET"
[simple-python]="GET"
[notes-python]="GET"
)
declare -A CHECK_URL=(
[hello-node]="https://sless-api.kube5s.ru/fn/default/hello-http"
[simple-node]="https://sless-api.kube5s.ru/fn/default/simple-node-time-display"
[simple-python]="https://sless-api.kube5s.ru/fn/default/simple-py-time-display"
[notes-python]="https://sless-api.kube5s.ru/fn/default/notes-list"
)
declare -A CHECK_DATA=(
[hello-node]='{"name":"Smoke"}'
[simple-node]=''
[simple-python]=''
[notes-python]=''
)
declare -a PREEXISTING_EXAMPLES=()
LAST_LOG_FILE=""
CURRENT_EXAMPLE=""
CURRENT_STEP=""
LAST_ERROR_SUMMARY=""
fail_run() {
local example="$1"
local step="$2"
local details="$3"
echo
echo "ERROR SUMMARY"
echo "example: $example"
echo "step: $step"
echo "reason: $details"
if [ -n "$LAST_LOG_FILE" ] && [ -f "$LAST_LOG_FILE" ]; then
echo "log: $LAST_LOG_FILE"
echo "last log lines:"
tail -n 20 "$LAST_LOG_FILE"
fi
exit 1
}
run_step() {
local example="$1"
local step="$2"
shift 2
CURRENT_EXAMPLE="$example"
CURRENT_STEP="$step"
LAST_ERROR_SUMMARY=""
if ! "$@"; then
local details="$LAST_ERROR_SUMMARY"
if [ -z "$details" ]; then
details="step failed without explicit summary"
fi
fail_run "$example" "$step" "$details"
fi
}
restore_any_backups() {
local backup
while IFS= read -r backup; do
[ -n "$backup" ] || continue
if [ -f "$backup" ]; then
mv "$backup" "${backup%.copilot.bak}"
fi
done < <(find "$ROOT_DIR" -name '*.copilot.bak' | sort)
}
trap restore_any_backups EXIT
clean_local_artifacts() {
local example="$1"
rm -rf \
"$ROOT_DIR/$example/.terraform" \
"$ROOT_DIR/$example/.terraform.lock.hcl" \
"$ROOT_DIR/$example/terraform.tfstate" \
"$ROOT_DIR/$example/terraform.tfstate.backup" \
"$ROOT_DIR/$example"/terraform.tfstate.*.backup \
"$ROOT_DIR/$example/dist"
}
retry_tf() {
local example="$1"
local label="$2"
shift 2
local attempt=1
while [ "$attempt" -le 3 ]; do
LAST_LOG_FILE="$LOG_DIR/${example//\//_}-${label// /_}-${attempt}.log"
echo "==> [$example] $label (attempt $attempt/3)"
(
cd "$ROOT_DIR/$example"
"$@"
) 2>&1 | tee "$LAST_LOG_FILE"
local status=${PIPESTATUS[0]}
if [ "$status" -eq 0 ]; then
return 0
fi
if grep -Eiq 'Unauthorized|401|403' "$LAST_LOG_FILE"; then
LAST_ERROR_SUMMARY="authorization error during $label"
echo "[$example] authorization error during $label"
return 41
fi
if grep -Eiq 'TLS handshake timeout|tls:.*timeout|i/o timeout|Client\.Timeout exceeded while awaiting headers|context deadline exceeded|unexpected EOF' "$LAST_LOG_FILE" && [ "$attempt" -lt 3 ]; then
attempt=$((attempt + 1))
sleep 2
continue
fi
if grep -Eiq 'TLS handshake timeout|tls:.*timeout|i/o timeout|Client\.Timeout exceeded while awaiting headers|context deadline exceeded|unexpected EOF' "$LAST_LOG_FILE"; then
LAST_ERROR_SUMMARY="network/provider download failure during $label after retries"
else
LAST_ERROR_SUMMARY="terraform command failed during $label with exit code $status"
fi
return "$status"
done
LAST_ERROR_SUMMARY="terraform command failed during $label after exhausting retries"
return 1
}
record_preexisting_if_needed() {
local example="$1"
if grep -Fq 'No changes. Your infrastructure matches the configuration.' "$LAST_LOG_FILE"; then
PREEXISTING_EXAMPLES+=("$example")
echo "[$example] detected preexisting remote resources on clean apply"
fi
}
probe_endpoint() {
local example="$1"
local body_file="$LOG_DIR/${example//\//_}-endpoint-body.txt"
local status_file="$LOG_DIR/${example//\//_}-endpoint-status.txt"
local method="${CHECK_METHOD[$example]}"
local url="${CHECK_URL[$example]}"
local data="${CHECK_DATA[$example]}"
if [ "$method" = "POST" ]; then
curl -sS -X POST -H 'Content-Type: application/json' -d "$data" -o "$body_file" -w '%{http_code}' "$url" > "$status_file"
else
curl -sS -o "$body_file" -w '%{http_code}' "$url" > "$status_file"
fi
}
assert_live_endpoint() {
local example="$1"
probe_endpoint "$example"
local body_file="$LOG_DIR/${example//\//_}-endpoint-body.txt"
local status
status="$(cat "$LOG_DIR/${example//\//_}-endpoint-status.txt")"
if [ "$status" != "200" ]; then
LAST_ERROR_SUMMARY="live endpoint check failed with HTTP $status"
echo "[$example] live endpoint check failed with HTTP $status"
cat "$body_file"
return 1
fi
if grep -Fq 'function unreachable' "$body_file"; then
LAST_ERROR_SUMMARY="live endpoint returned function unreachable"
echo "[$example] live endpoint check returned unreachable function"
cat "$body_file"
return 1
fi
}
assert_destroyed_endpoint() {
local example="$1"
probe_endpoint "$example"
local body_file="$LOG_DIR/${example//\//_}-endpoint-body.txt"
local status
status="$(cat "$LOG_DIR/${example//\//_}-endpoint-status.txt")"
if [ "$status" = "404" ] || [ "$status" = "000" ]; then
return 0
fi
if grep -Eiq 'not found|404 page not found' "$body_file"; then
return 0
fi
if [ "$status" = "502" ] && grep -Fq 'function unreachable' "$body_file"; then
LAST_ERROR_SUMMARY="route cleanup bug: public endpoint still exists but backend is already gone (HTTP 502 function unreachable)"
echo "[$example] route still exists after destroy, but backend is already gone (HTTP 502 function unreachable)"
cat "$body_file"
return 1
fi
LAST_ERROR_SUMMARY="endpoint still responds after destroy with HTTP $status"
echo "[$example] endpoint still responds after destroy with HTTP $status"
cat "$body_file"
return 1
}
wait_for_destroyed_endpoint() {
local example="$1"
local attempts=24
local sleep_sec=5
local try=1
while [ "$try" -le "$attempts" ]; do
if assert_destroyed_endpoint "$example"; then
echo "[$example] endpoint disappeared after destroy"
return 0
fi
echo "[$example] endpoint still present after destroy, waiting (${try}/${attempts})"
try=$((try + 1))
sleep "$sleep_sec"
done
echo "[$example] endpoint did not disappear after destroy within $((attempts * sleep_sec))s"
if [ -z "$LAST_ERROR_SUMMARY" ]; then
LAST_ERROR_SUMMARY="endpoint remained reachable for more than $((attempts * sleep_sec))s after destroy"
else
LAST_ERROR_SUMMARY="$LAST_ERROR_SUMMARY; endpoint was still published after $((attempts * sleep_sec))s"
fi
return 1
}
backup_and_modify() {
local example="$1"
case "$example" in
hello-node)
cp "$ROOT_DIR/$example/http.tf" "$ROOT_DIR/$example/http.tf.copilot.bak"
perl -0pi -e 's/enabled\s+=\s+true/enabled = false/' "$ROOT_DIR/$example/http.tf"
;;
simple-node)
cp "$ROOT_DIR/$example/time-display.tf" "$ROOT_DIR/$example/time-display.tf.copilot.bak"
perl -0pi -e 's/memory_mb\s+=\s+64/memory_mb = 96/' "$ROOT_DIR/$example/time-display.tf"
;;
simple-python)
cp "$ROOT_DIR/$example/time-display.tf" "$ROOT_DIR/$example/time-display.tf.copilot.bak"
perl -0pi -e 's/memory_mb\s+=\s+64/memory_mb = 96/' "$ROOT_DIR/$example/time-display.tf"
;;
notes-python)
cp "$ROOT_DIR/$example/notes-list.tf" "$ROOT_DIR/$example/notes-list.tf.copilot.bak"
perl -0pi -e 's/memory_mb\s+=\s+128/memory_mb = 160/' "$ROOT_DIR/$example/notes-list.tf"
;;
esac
}
restore_modified_files() {
local example="$1"
case "$example" in
hello-node)
mv "$ROOT_DIR/$example/http.tf.copilot.bak" "$ROOT_DIR/$example/http.tf"
;;
simple-node)
mv "$ROOT_DIR/$example/time-display.tf.copilot.bak" "$ROOT_DIR/$example/time-display.tf"
;;
simple-python)
mv "$ROOT_DIR/$example/time-display.tf.copilot.bak" "$ROOT_DIR/$example/time-display.tf"
;;
notes-python)
mv "$ROOT_DIR/$example/notes-list.tf.copilot.bak" "$ROOT_DIR/$example/notes-list.tf"
;;
esac
}
run_example() {
local example="$1"
echo
echo "==== $example ===="
clean_local_artifacts "$example"
run_step "$example" "terraform init" retry_tf "$example" "terraform init" terraform init -input=false -no-color
run_step "$example" "terraform apply clean" retry_tf "$example" "terraform apply clean" terraform apply -auto-approve -input=false -no-color
record_preexisting_if_needed "$example"
run_step "$example" "endpoint check after clean apply" assert_live_endpoint "$example"
run_step "$example" "terraform destroy clean" retry_tf "$example" "terraform destroy clean" terraform destroy -auto-approve -input=false -no-color
run_step "$example" "endpoint cleanup after clean destroy" wait_for_destroyed_endpoint "$example"
run_step "$example" "terraform apply second" retry_tf "$example" "terraform apply second" terraform apply -auto-approve -input=false -no-color
run_step "$example" "endpoint check after second apply" assert_live_endpoint "$example"
backup_and_modify "$example"
run_step "$example" "terraform apply modified" retry_tf "$example" "terraform apply modified" terraform apply -auto-approve -input=false -no-color
restore_modified_files "$example"
run_step "$example" "terraform destroy final" retry_tf "$example" "terraform destroy final" terraform destroy -auto-approve -input=false -no-color
run_step "$example" "endpoint cleanup after final destroy" wait_for_destroyed_endpoint "$example"
clean_local_artifacts "$example"
}
main() {
local example
for example in "${EXAMPLES[@]}"; do
run_example "$example"
done
if [ "${#PREEXISTING_EXAMPLES[@]}" -gt 0 ]; then
echo
echo "Preexisting remote resources were detected on first apply for: ${PREEXISTING_EXAMPLES[*]}"
exit 2
fi
echo
echo "All Terraform example lifecycles completed successfully."
}
main "$@"