a1a17e81a1
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
589 lines
23 KiB
Bash
Executable File
589 lines
23 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
#########################################################################################
|
|
#### terraform-lint-reporter.sh — Lint and plan Terraform configs with formatted ####
|
|
#### reports. Wraps tflint and terraform plan with severity levels and CI exit codes ####
|
|
#### Requires: bash 4+, terraform, tflint ####
|
|
#### ####
|
|
#### Author: Phil Connor ####
|
|
#### Contact: contact@mylinux.work ####
|
|
#### License: MIT ####
|
|
#### Version 1.01 ####
|
|
#### ####
|
|
#### Usage: ####
|
|
#### ./terraform-lint-reporter.sh --full --dir ./infra ####
|
|
#### ####
|
|
#### See --help for all options. ####
|
|
#########################################################################################
|
|
# v1.01 changes:
|
|
# - Fixed: ((0++)) returns 1 under set -e; added || true guards
|
|
# - Fixed: grep in pipeline crashes under set -euo pipefail when no matches found. Added || true guard
|
|
#########################################################################################
|
|
|
|
set -euo pipefail
|
|
|
|
# ── Defaults ──────────────────────────────────────────────────────────
|
|
RUN_MODE=""
|
|
TF_DIR="${TF_DIR:-.}"
|
|
TFLINT_PATH="${TFLINT_PATH:-tflint}"
|
|
TF_PATH="${TF_PATH:-terraform}"
|
|
OUTPUT_FORMAT="${TF_REPORT_FORMAT:-text}"
|
|
OUTPUT_FILE=""
|
|
FAIL_ON="${TF_FAIL_ON:-error}"
|
|
AUTO_INIT="${TF_AUTO_INIT:-true}"
|
|
PLAN_FILE=""
|
|
VAR_FILE=""
|
|
VERBOSE="${VERBOSE:-false}"
|
|
COLOR="${COLOR:-auto}"
|
|
|
|
# ── State ─────────────────────────────────────────────────────────────
|
|
SCRIPT_NAME="$(basename "$0")"
|
|
readonly SCRIPT_NAME
|
|
START_TIME=""
|
|
LINT_WARNINGS=0
|
|
LINT_ERRORS=0
|
|
PLAN_ADDS=0
|
|
PLAN_CHANGES=0
|
|
PLAN_DESTROYS=0
|
|
TMPDIR_WORK=""
|
|
RESULTS_JSON="[]"
|
|
|
|
# ── Colors ────────────────────────────────────────────────────────────
|
|
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" DIM="" RESET=""
|
|
|
|
setup_colors() {
|
|
if [[ "$COLOR" == "never" ]]; then
|
|
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" DIM="" RESET=""
|
|
return
|
|
fi
|
|
if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[0;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
BOLD='\033[1m'
|
|
DIM='\033[2m'
|
|
RESET='\033[0m'
|
|
else
|
|
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" DIM="" RESET=""
|
|
fi
|
|
}
|
|
|
|
# ── Logging ───────────────────────────────────────────────────────────
|
|
log() { echo -e "${BLUE}[INFO]${RESET} $*"; }
|
|
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
|
|
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
|
|
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${DIM}[DEBUG]${RESET} $*"; fi; }
|
|
die() { err "$*"; exit 1; }
|
|
|
|
# ── Helpers ───────────────────────────────────────────────────────────
|
|
section_header() {
|
|
echo ""
|
|
echo -e " ${BOLD}${CYAN}── $1 ──${RESET}"
|
|
echo ""
|
|
}
|
|
|
|
field() {
|
|
printf " ${BOLD}%-22s${RESET} %s\n" "$1" "$2"
|
|
}
|
|
|
|
field_color() {
|
|
printf " ${BOLD}%-22s${RESET} %b\n" "$1" "$2"
|
|
}
|
|
|
|
elapsed() {
|
|
local end_time
|
|
end_time=$(date +%s)
|
|
echo "$(( end_time - START_TIME ))s"
|
|
}
|
|
|
|
cleanup() {
|
|
if [[ -n "$TMPDIR_WORK" && -d "$TMPDIR_WORK" ]]; then
|
|
rm -rf "$TMPDIR_WORK"
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
make_tmpdir() {
|
|
if [[ -z "$TMPDIR_WORK" ]]; then
|
|
TMPDIR_WORK=$(mktemp -d "${TMPDIR:-/tmp}/tf-lint-reporter.XXXXXX")
|
|
fi
|
|
}
|
|
|
|
# ── Dependency checks ────────────────────────────────────────────────
|
|
require_terraform() {
|
|
if ! command -v "$TF_PATH" &>/dev/null; then
|
|
die "Terraform binary not found: ${TF_PATH}"
|
|
fi
|
|
verbose "Terraform found: $(command -v "$TF_PATH") ($("$TF_PATH" version -json 2>/dev/null | jq -r '.terraform_version' 2>/dev/null || "$TF_PATH" version | head -1))"
|
|
}
|
|
|
|
require_tflint() {
|
|
if ! command -v "$TFLINT_PATH" &>/dev/null; then
|
|
die "tflint binary not found: ${TFLINT_PATH} — install from https://github.com/terraform-linters/tflint"
|
|
fi
|
|
verbose "tflint found: $(command -v "$TFLINT_PATH") ($("$TFLINT_PATH" --version 2>/dev/null || echo 'unknown'))"
|
|
}
|
|
|
|
# ── Lint ─────────────────────────────────────────────────────────────
|
|
do_lint() {
|
|
require_tflint
|
|
|
|
START_TIME=$(date +%s)
|
|
section_header "Terraform Lint (tflint)"
|
|
field "Directory:" "$TF_DIR"
|
|
echo ""
|
|
|
|
local lint_output exit_code=0
|
|
verbose "Running: ${TFLINT_PATH} --format json --chdir ${TF_DIR}"
|
|
lint_output=$("$TFLINT_PATH" --format json --chdir "$TF_DIR" 2>&1) || exit_code=$?
|
|
|
|
local issues_count=0
|
|
|
|
if command -v jq &>/dev/null && echo "$lint_output" | jq empty 2>/dev/null; then
|
|
issues_count=$(echo "$lint_output" | jq '[.issues // [] | .[] ] | length' 2>/dev/null || echo 0)
|
|
|
|
if [[ "$issues_count" -gt 0 ]]; then
|
|
printf " ${BOLD}%-10s %-30s %-8s %s${RESET}\n" "SEVERITY" "RULE" "LINE" "MESSAGE"
|
|
printf " %-10s %-30s %-8s %s\n" "----------" "------------------------------" "--------" "$(printf '%0.s-' {1..30})"
|
|
|
|
echo "$lint_output" | jq -r '.issues[] | [.rule.severity, .rule.name, (.range.filename + ":" + (.range.start.line | tostring)), .message] | @tsv' 2>/dev/null | \
|
|
while IFS=$'\t' read -r sev rule loc msg; do
|
|
local color="$RESET"
|
|
case "$sev" in
|
|
error) color="$RED"; ((LINT_ERRORS++)) || true ;;
|
|
warning) color="$YELLOW"; ((LINT_WARNINGS++)) || true ;;
|
|
notice) color="$DIM" ;;
|
|
esac
|
|
printf " ${color}%-10s${RESET} %-30s %-8s %s\n" "${sev^^}" "$rule" "$loc" "$msg"
|
|
|
|
RESULTS_JSON=$(echo "$RESULTS_JSON" | jq --arg s "$sev" --arg r "$rule" \
|
|
--arg l "$loc" --arg m "$msg" \
|
|
'. + [{"type":"lint","severity":$s,"rule":$r,"location":$l,"message":$m}]')
|
|
done
|
|
fi
|
|
else
|
|
while IFS= read -r line; do
|
|
if [[ "$line" =~ (Error|error) ]]; then
|
|
((LINT_ERRORS++)) || true
|
|
echo -e " ${RED}${line}${RESET}"
|
|
elif [[ "$line" =~ (Warning|warning) ]]; then
|
|
((LINT_WARNINGS++)) || true
|
|
echo -e " ${YELLOW}${line}${RESET}"
|
|
elif [[ -n "$line" ]]; then
|
|
echo " $line"
|
|
fi
|
|
done <<< "$lint_output"
|
|
fi
|
|
|
|
local tf_files=0
|
|
if [[ -d "$TF_DIR" ]]; then
|
|
tf_files=$(find "$TF_DIR" -maxdepth 1 -name '*.tf' | wc -l | tr -d ' ')
|
|
fi
|
|
|
|
section_header "Lint Summary"
|
|
field "Files checked:" "$tf_files"
|
|
field_color "Errors:" "${RED}${LINT_ERRORS}${RESET}"
|
|
field_color "Warnings:" "${YELLOW}${LINT_WARNINGS}${RESET}"
|
|
field "Duration:" "$(elapsed)"
|
|
}
|
|
|
|
# ── Plan ─────────────────────────────────────────────────────────────
|
|
do_plan() {
|
|
require_terraform
|
|
|
|
START_TIME=$(date +%s)
|
|
section_header "Terraform Plan"
|
|
field "Directory:" "$TF_DIR"
|
|
field "Auto-init:" "$AUTO_INIT"
|
|
[[ -n "$VAR_FILE" ]] && field "Var file:" "$VAR_FILE"
|
|
echo ""
|
|
|
|
if [[ "$AUTO_INIT" == "true" ]]; then
|
|
log "Running terraform init..."
|
|
verbose "Running: ${TF_PATH} -chdir=${TF_DIR} init -input=false -no-color"
|
|
if ! "$TF_PATH" -chdir="$TF_DIR" init -input=false -no-color >/dev/null 2>&1; then
|
|
die "terraform init failed in ${TF_DIR}"
|
|
fi
|
|
log "Init complete"
|
|
fi
|
|
|
|
make_tmpdir
|
|
local plan_out="${TMPDIR_WORK}/tfplan"
|
|
local plan_args=(-detailed-exitcode -no-color -out="$plan_out")
|
|
[[ -n "$VAR_FILE" ]] && plan_args+=(-var-file="$VAR_FILE")
|
|
|
|
local plan_output plan_exit=0
|
|
verbose "Running: ${TF_PATH} -chdir=${TF_DIR} plan ${plan_args[*]}"
|
|
plan_output=$("$TF_PATH" -chdir="$TF_DIR" plan "${plan_args[@]}" 2>&1) || plan_exit=$?
|
|
|
|
verbose "terraform plan exit code: ${plan_exit}"
|
|
|
|
if [[ $plan_exit -eq 1 ]]; then
|
|
err "terraform plan failed:"
|
|
echo "$plan_output" | tail -20
|
|
exit 1
|
|
fi
|
|
|
|
if echo "$plan_output" | grep -qE 'Plan: [0-9]+ to add'; then
|
|
local plan_line
|
|
plan_line=$(echo "$plan_output" | { grep -oE 'Plan: [0-9]+ to add, [0-9]+ to change, [0-9]+ to destroy' || true; })
|
|
if [[ -n "$plan_line" ]]; then
|
|
PLAN_ADDS=$(echo "$plan_line" | { grep -oE '[0-9]+ to add' || true; } | { grep -oE '[0-9]+' || true; })
|
|
PLAN_CHANGES=$(echo "$plan_line" | { grep -oE '[0-9]+ to change' || true; } | { grep -oE '[0-9]+' || true; })
|
|
PLAN_DESTROYS=$(echo "$plan_line" | { grep -oE '[0-9]+ to destroy' || true; } | { grep -oE '[0-9]+' || true; })
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$PLAN_FILE" ]]; then
|
|
cp "$plan_out" "$PLAN_FILE"
|
|
log "Plan file saved to ${PLAN_FILE}"
|
|
fi
|
|
|
|
RESULTS_JSON=$(echo "$RESULTS_JSON" | jq \
|
|
--argjson a "$PLAN_ADDS" --argjson c "$PLAN_CHANGES" --argjson d "$PLAN_DESTROYS" \
|
|
'. + [{"type":"plan","adds":$a,"changes":$c,"destroys":$d}]')
|
|
|
|
section_header "Plan Summary"
|
|
if [[ $plan_exit -eq 0 ]]; then
|
|
echo -e " ${GREEN}No changes.${RESET} Infrastructure is up-to-date."
|
|
else
|
|
field_color "Resources to add:" "${GREEN}${PLAN_ADDS}${RESET}"
|
|
field_color "Resources to change:" "${YELLOW}${PLAN_CHANGES}${RESET}"
|
|
field_color "Resources to destroy:" "${RED}${PLAN_DESTROYS}${RESET}"
|
|
fi
|
|
field "Duration:" "$(elapsed)"
|
|
}
|
|
|
|
# ── Full ─────────────────────────────────────────────────────────────
|
|
do_full() {
|
|
START_TIME=$(date +%s)
|
|
local full_start
|
|
full_start=$(date +%s)
|
|
|
|
do_lint
|
|
echo ""
|
|
do_plan
|
|
|
|
section_header "Combined Summary"
|
|
field_color "Lint errors:" "${RED}${LINT_ERRORS}${RESET}"
|
|
field_color "Lint warnings:" "${YELLOW}${LINT_WARNINGS}${RESET}"
|
|
field_color "Plan adds:" "${GREEN}${PLAN_ADDS}${RESET}"
|
|
field_color "Plan changes:" "${YELLOW}${PLAN_CHANGES}${RESET}"
|
|
field_color "Plan destroys:" "${RED}${PLAN_DESTROYS}${RESET}"
|
|
local total_elapsed=$(( $(date +%s) - full_start ))
|
|
field "Total duration:" "${total_elapsed}s"
|
|
}
|
|
|
|
# ── Fmt ──────────────────────────────────────────────────────────────
|
|
do_fmt() {
|
|
require_terraform
|
|
|
|
START_TIME=$(date +%s)
|
|
section_header "Terraform Format Check"
|
|
field "Directory:" "$TF_DIR"
|
|
echo ""
|
|
|
|
local fmt_output exit_code=0
|
|
verbose "Running: ${TF_PATH} fmt -check -diff -recursive ${TF_DIR}"
|
|
fmt_output=$("$TF_PATH" fmt -check -diff -recursive "$TF_DIR" 2>&1) || exit_code=$?
|
|
|
|
local unformatted=0
|
|
local unformatted_files=()
|
|
|
|
if [[ $exit_code -ne 0 && -n "$fmt_output" ]]; then
|
|
while IFS= read -r line; do
|
|
if [[ "$line" =~ \.tf$ ]]; then
|
|
((unformatted++)) || true
|
|
unformatted_files+=("$line")
|
|
echo -e " ${YELLOW}needs formatting:${RESET} ${line}"
|
|
fi
|
|
done <<< "$fmt_output"
|
|
fi
|
|
|
|
section_header "Format Summary"
|
|
if [[ $unformatted -eq 0 ]]; then
|
|
echo -e " ${GREEN}✓${RESET} All files are properly formatted"
|
|
else
|
|
field_color "Files needing format:" "${YELLOW}${unformatted}${RESET}"
|
|
echo ""
|
|
echo -e " Run ${BOLD}${TF_PATH} fmt -recursive ${TF_DIR}${RESET} to fix"
|
|
fi
|
|
field "Duration:" "$(elapsed)"
|
|
|
|
for f in "${unformatted_files[@]+"${unformatted_files[@]}"}"; do
|
|
RESULTS_JSON=$(echo "$RESULTS_JSON" | jq --arg f "$f" \
|
|
'. + [{"type":"fmt","file":$f,"message":"needs formatting"}]')
|
|
done
|
|
}
|
|
|
|
# ── Validate ─────────────────────────────────────────────────────────
|
|
do_validate() {
|
|
require_terraform
|
|
|
|
START_TIME=$(date +%s)
|
|
section_header "Terraform Validate"
|
|
field "Directory:" "$TF_DIR"
|
|
echo ""
|
|
|
|
if [[ "$AUTO_INIT" == "true" ]]; then
|
|
log "Running terraform init -backend=false..."
|
|
if ! "$TF_PATH" -chdir="$TF_DIR" init -backend=false -input=false -no-color >/dev/null 2>&1; then
|
|
die "terraform init failed in ${TF_DIR}"
|
|
fi
|
|
fi
|
|
|
|
local validate_output exit_code=0
|
|
verbose "Running: ${TF_PATH} -chdir=${TF_DIR} validate -json"
|
|
validate_output=$("$TF_PATH" -chdir="$TF_DIR" validate -json 2>&1) || exit_code=$?
|
|
|
|
local valid diag_count
|
|
valid=$(echo "$validate_output" | jq -r '.valid' 2>/dev/null || echo "false")
|
|
diag_count=$(echo "$validate_output" | jq '[.diagnostics // [] | .[]] | length' 2>/dev/null || echo 0)
|
|
|
|
if [[ "$valid" == "true" ]]; then
|
|
echo -e " ${GREEN}✓${RESET} Configuration is valid"
|
|
else
|
|
echo -e " ${RED}✗${RESET} Configuration has errors"
|
|
echo ""
|
|
|
|
if [[ "$diag_count" -gt 0 ]]; then
|
|
echo "$validate_output" | jq -r '.diagnostics[] | [.severity, .summary, .detail] | @tsv' 2>/dev/null | \
|
|
while IFS=$'\t' read -r sev summary detail; do
|
|
local color="$RESET"
|
|
case "$sev" in
|
|
error) color="$RED" ;;
|
|
warning) color="$YELLOW" ;;
|
|
esac
|
|
echo -e " ${color}[${sev^^}]${RESET} ${summary}"
|
|
[[ -n "$detail" ]] && echo " $detail"
|
|
|
|
RESULTS_JSON=$(echo "$RESULTS_JSON" | jq --arg s "$sev" \
|
|
--arg m "$summary" --arg d "$detail" \
|
|
'. + [{"type":"validate","severity":$s,"summary":$m,"detail":$d}]')
|
|
done
|
|
fi
|
|
fi
|
|
|
|
section_header "Validation Summary"
|
|
field "Valid:" "$valid"
|
|
field "Diagnostics:" "$diag_count"
|
|
field "Duration:" "$(elapsed)"
|
|
}
|
|
|
|
# ── Output formats ───────────────────────────────────────────────────
|
|
write_output() {
|
|
[[ -z "$OUTPUT_FILE" ]] && return
|
|
case "$OUTPUT_FORMAT" in
|
|
json) write_json ;;
|
|
junit) write_junit ;;
|
|
text) write_text ;;
|
|
*) die "Unknown output format: ${OUTPUT_FORMAT}" ;;
|
|
esac
|
|
log "Results written to ${OUTPUT_FILE}"
|
|
}
|
|
|
|
write_json() {
|
|
jq -n --argjson results "$RESULTS_JSON" \
|
|
--argjson lint_errors "$LINT_ERRORS" \
|
|
--argjson lint_warnings "$LINT_WARNINGS" \
|
|
--argjson plan_adds "$PLAN_ADDS" \
|
|
--argjson plan_changes "$PLAN_CHANGES" \
|
|
--argjson plan_destroys "$PLAN_DESTROYS" \
|
|
--arg dir "$TF_DIR" \
|
|
--arg mode "$RUN_MODE" \
|
|
'{
|
|
mode: $mode,
|
|
directory: $dir,
|
|
summary: {
|
|
lint_errors: $lint_errors,
|
|
lint_warnings: $lint_warnings,
|
|
plan_adds: $plan_adds,
|
|
plan_changes: $plan_changes,
|
|
plan_destroys: $plan_destroys
|
|
},
|
|
results: $results
|
|
}' > "$OUTPUT_FILE"
|
|
}
|
|
|
|
write_junit() {
|
|
local total_tests failures
|
|
total_tests=$(echo "$RESULTS_JSON" | jq '[.[] | select(.type == "lint")] | length' 2>/dev/null || echo 0)
|
|
failures=$LINT_ERRORS
|
|
|
|
{
|
|
echo '<?xml version="1.0" encoding="UTF-8"?>'
|
|
echo "<testsuites tests=\"${total_tests}\" failures=\"${failures}\">"
|
|
echo " <testsuite name=\"terraform-lint-reporter\" tests=\"${total_tests}\" failures=\"${failures}\">"
|
|
|
|
echo "$RESULTS_JSON" | jq -r '.[] | select(.type == "lint") | @base64' 2>/dev/null | while read -r entry; do
|
|
local sev rule loc msg
|
|
sev=$(echo "$entry" | base64 -d | jq -r '.severity')
|
|
rule=$(echo "$entry" | base64 -d | jq -r '.rule')
|
|
loc=$(echo "$entry" | base64 -d | jq -r '.location')
|
|
msg=$(echo "$entry" | base64 -d | jq -r '.message')
|
|
if [[ "$sev" == "error" ]]; then
|
|
echo " <testcase name=\"${rule}\" classname=\"${loc}\">"
|
|
echo " <failure message=\"${msg}\" type=\"${sev}\"/>"
|
|
echo " </testcase>"
|
|
else
|
|
echo " <testcase name=\"${rule}\" classname=\"${loc}\"/>"
|
|
fi
|
|
done
|
|
|
|
echo " </testsuite>"
|
|
echo "</testsuites>"
|
|
} > "$OUTPUT_FILE"
|
|
}
|
|
|
|
write_text() {
|
|
{
|
|
echo "Terraform Lint Reporter Results"
|
|
echo "==============================="
|
|
echo ""
|
|
echo "Mode: ${RUN_MODE}"
|
|
echo "Directory: ${TF_DIR}"
|
|
echo ""
|
|
echo "$RESULTS_JSON" | jq -r '.[] | if .type == "lint" then
|
|
"[\(.severity | ascii_upcase)] \(.rule) at \(.location): \(.message)"
|
|
elif .type == "plan" then
|
|
"Plan: \(.adds) to add, \(.changes) to change, \(.destroys) to destroy"
|
|
elif .type == "fmt" then
|
|
"[FMT] \(.file): \(.message)"
|
|
elif .type == "validate" then
|
|
"[\(.severity | ascii_upcase)] \(.summary)"
|
|
else empty end' 2>/dev/null || true
|
|
echo ""
|
|
echo "Lint Errors: ${LINT_ERRORS} Warnings: ${LINT_WARNINGS}"
|
|
echo "Plan: +${PLAN_ADDS} ~${PLAN_CHANGES} -${PLAN_DESTROYS}"
|
|
} > "$OUTPUT_FILE"
|
|
}
|
|
|
|
# ── Compute exit code ────────────────────────────────────────────────
|
|
compute_exit() {
|
|
case "$FAIL_ON" in
|
|
warning)
|
|
if [[ $LINT_ERRORS -gt 0 || $LINT_WARNINGS -gt 0 ]]; then
|
|
exit 2
|
|
fi
|
|
if [[ $PLAN_DESTROYS -gt 0 || $PLAN_CHANGES -gt 0 ]]; then
|
|
exit 2
|
|
fi
|
|
;;
|
|
error)
|
|
if [[ $LINT_ERRORS -gt 0 ]]; then
|
|
exit 2
|
|
fi
|
|
if [[ $PLAN_DESTROYS -gt 0 ]]; then
|
|
exit 2
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ── Usage ─────────────────────────────────────────────────────────────
|
|
show_help() {
|
|
cat <<EOF
|
|
${BOLD}${SCRIPT_NAME}${RESET} — Terraform Lint & Plan Reporter
|
|
|
|
Lint and plan Terraform configurations with formatted reports,
|
|
severity levels, and CI-friendly exit codes.
|
|
|
|
${BOLD}MODES${RESET}
|
|
--lint Run tflint on Terraform directory
|
|
--plan Run terraform init + plan, report resource changes
|
|
--full Run lint then plan, combined report
|
|
--fmt Check formatting with terraform fmt
|
|
--validate Run terraform validate, report diagnostics
|
|
|
|
${BOLD}OPTIONS${RESET}
|
|
--dir DIR Terraform directory (default: .)
|
|
--var-file FILE Terraform var file for plan
|
|
--plan-out FILE Save plan binary to file
|
|
--format FORMAT Output format: text, json, junit (default: text)
|
|
--output-file FILE Write results to file
|
|
--fail-on LEVEL Fail threshold: warning or error (default: error)
|
|
--no-init Skip automatic terraform init
|
|
--verbose Show debug output
|
|
--no-color Disable colored output
|
|
--help Show this help message
|
|
|
|
${BOLD}ENVIRONMENT VARIABLES${RESET}
|
|
TF_DIR Default Terraform directory
|
|
TF_PATH Path to terraform binary (default: terraform)
|
|
TFLINT_PATH Path to tflint binary (default: tflint)
|
|
TF_REPORT_FORMAT Default output format
|
|
TF_FAIL_ON Default fail threshold
|
|
TF_AUTO_INIT Auto-run terraform init (default: true)
|
|
VERBOSE Enable verbose output (true/false)
|
|
COLOR Color mode: auto, always, never
|
|
|
|
${BOLD}EXAMPLES${RESET}
|
|
# Lint only
|
|
${SCRIPT_NAME} --lint --dir ./infra
|
|
|
|
# Plan with var file
|
|
${SCRIPT_NAME} --plan --dir ./infra --var-file prod.tfvars
|
|
|
|
# Full lint + plan, save JSON report
|
|
${SCRIPT_NAME} --full --dir ./infra --format json --output-file report.json
|
|
|
|
# Format check in CI
|
|
${SCRIPT_NAME} --fmt --dir ./modules
|
|
|
|
# Validate configuration
|
|
${SCRIPT_NAME} --validate --dir ./infra
|
|
|
|
# Strict CI pipeline (fail on warnings)
|
|
${SCRIPT_NAME} --full --dir ./infra --fail-on warning --format junit --output-file results.xml
|
|
|
|
${BOLD}EXIT CODES${RESET}
|
|
0 All checks passed
|
|
1 Runtime error
|
|
2 Lint errors or plan changes exceed threshold
|
|
EOF
|
|
}
|
|
|
|
# ── Parse arguments ──────────────────────────────────────────────────
|
|
parse_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--lint) RUN_MODE="lint"; shift ;;
|
|
--plan) RUN_MODE="plan"; shift ;;
|
|
--full) RUN_MODE="full"; shift ;;
|
|
--fmt) RUN_MODE="fmt"; shift ;;
|
|
--validate) RUN_MODE="validate"; shift ;;
|
|
--dir) TF_DIR="${2:?--dir requires a path}"; shift 2 ;;
|
|
--var-file) VAR_FILE="${2:?--var-file requires a path}"; shift 2 ;;
|
|
--plan-out) PLAN_FILE="${2:?--plan-out requires a path}"; shift 2 ;;
|
|
--format) OUTPUT_FORMAT="${2:?--format requires a value}"; shift 2 ;;
|
|
--output-file) OUTPUT_FILE="${2:?--output-file requires a path}"; shift 2 ;;
|
|
--fail-on) FAIL_ON="${2:?--fail-on requires a value}"; shift 2 ;;
|
|
--no-init) AUTO_INIT="false"; shift ;;
|
|
--verbose) VERBOSE="true"; shift ;;
|
|
--no-color) COLOR="never"; shift ;;
|
|
--help|-h) setup_colors; show_help; exit 0 ;;
|
|
*) die "Unknown option: $1 (see --help)" ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# ── Main ─────────────────────────────────────────────────────────────
|
|
main() {
|
|
parse_args "$@"
|
|
setup_colors
|
|
|
|
case "$RUN_MODE" in
|
|
lint) do_lint; write_output; compute_exit ;;
|
|
plan) do_plan; write_output; compute_exit ;;
|
|
full) do_full; write_output; compute_exit ;;
|
|
fmt) do_fmt; write_output; compute_exit ;;
|
|
validate) do_validate; write_output; compute_exit ;;
|
|
"") err "No mode specified"; echo ""; show_help; exit 1 ;;
|
|
*) die "Unknown mode: ${RUN_MODE}" ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|