From 8fa078d61dbc8792ece4321ebd2fdebabc92f275 Mon Sep 17 00:00:00 2001 From: chrischristiansen-glitch Date: Sat, 23 May 2026 07:18:19 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20add=20scripts/cost-audit.sh=20=E2=80=94?= =?UTF-8?q?=20billable=20resource=20snapshot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/cost-alerts.sh | 50 +++++++++++ scripts/cost-audit.sh | 196 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 scripts/cost-alerts.sh create mode 100644 scripts/cost-audit.sh diff --git a/scripts/cost-alerts.sh b/scripts/cost-alerts.sh new file mode 100644 index 0000000..6337322 --- /dev/null +++ b/scripts/cost-alerts.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# ============================================================================= +# cost-alerts.sh — Nightly check: exits non-zero + prints red warning +# if any always-on resources are found. +# Designed for cron or quick end-of-day terminal check. +# Usage: source .env && bash scripts/cost-alerts.sh +# ============================================================================= +set -euo pipefail + +: "${PROJECT_ID:?Set PROJECT_ID}" +: "${REGION:?Set REGION}" + +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' + +RISK=0 + +check() { + local LABEL="$1"; local CMD="$2" + RESULT=$(eval "$CMD" 2>/dev/null || true) + if [[ -n "$RESULT" ]]; then + printf "${RED}[!!] %-22s ACTIVE: %s${NC}\n" "$LABEL" "$(echo "$RESULT" | head -3 | tr '\n' ' ')" + RISK=1 + fi +} + +printf "\n OSVauco Cost Alert Check — $(date '+%Y-%m-%d %H:%M')\n\n" + +check "Compute VMs" \ + "gcloud compute instances list --project=${PROJECT_ID} --format='value(name,status)' | grep RUNNING" + +check "Cloud SQL" \ + "gcloud sql instances list --project=${PROJECT_ID} --format='value(name,state)' | grep RUNNABLE" + +check "Composer" \ + "gcloud composer environments list --project=${PROJECT_ID} --locations=${REGION} --format='value(name)'" + +check "Memory Bank" \ + "gcloud ai reasoning-engines list --project=${PROJECT_ID} --region=${REGION} --format='value(name)'" + +check "Cloud Run min>0" \ + "gcloud run services list --project=${PROJECT_ID} --region=${REGION} --format='value(name)' | xargs -I{} gcloud run services describe {} --project=${PROJECT_ID} --region=${REGION} --format='value(spec.template.metadata.annotations.autoscaling\.knative\.dev/minScale)' 2>/dev/null | grep -v '^$' | grep -v '^0$'" + +if [[ $RISK -eq 0 ]]; then + printf " ${GREEN}[OK] No always-on resources detected. Sleep well.${NC}\n\n" +else + printf "\n ${RED}Run: bash scripts/cost-audit.sh for full details.${NC}\n\n" + exit 1 +fi diff --git a/scripts/cost-audit.sh b/scripts/cost-audit.sh new file mode 100644 index 0000000..833245f --- /dev/null +++ b/scripts/cost-audit.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash +# ============================================================================= +# cost-audit.sh — OSVauco billable resource snapshot +# Read-only, safe to run anytime. +# Usage: source .env && bash scripts/cost-audit.sh +# ============================================================================= +set -euo pipefail + +: "${PROJECT_ID:?Set PROJECT_ID}" +: "${REGION:?Set REGION}" + +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +DIM='\033[2m' +BOLD='\033[1m' +NC='\033[0m' + +OK="${GREEN}[OK]${NC}" +WARN="${YELLOW}[!!]${NC}" +FAIL="${RED}[XX]${NC}" +DOT="${DIM}[--]${NC}" + +clear +printf "${CYAN}${BOLD}" +echo " -------------------------------------------------------" +echo " OSVauco COST AUDIT" +printf " -------------------------------------------------------${NC}\n" +printf " ${DIM}Project : ${PROJECT_ID} | Region : ${REGION}${NC}\n" +printf " ${DIM}$(date '+%Y-%m-%d %H:%M %Z')${NC}\n" +echo "" + +ANY_RISK=false + +# ----------------------------------------------------------------------------- +# 1. CLOUD RUN +# ----------------------------------------------------------------------------- +printf "${BOLD}${CYAN} [ CLOUD RUN ]${NC} ${DIM}(scale-to-zero = no idle cost)${NC}\n" +CR=$(gcloud run services list \ + --project="${PROJECT_ID}" \ + --region="${REGION}" \ + --format="value(name,status.url)" 2>/dev/null || true) + +if [[ -z "$CR" ]]; then + printf " ${OK} No services deployed\n" +else + while IFS=$'\t' read -r NAME URL; do + # Check min-instances annotation + MIN=$(gcloud run services describe "$NAME" \ + --project="${PROJECT_ID}" --region="${REGION}" \ + --format="value(spec.template.metadata.annotations.'autoscaling.knative.dev/minScale')" \ + 2>/dev/null || echo "0") + MIN=${MIN:-0} + if [[ "$MIN" -gt 0 ]]; then + printf " ${WARN} %-30s ${YELLOW}min-instances=%s (idle cost!)${NC}\n" "$NAME" "$MIN" + ANY_RISK=true + else + printf " ${OK} %-30s ${DIM}scale-to-zero${NC}\n" "$NAME" + fi + done <<< "$CR" +fi +echo "" + +# ----------------------------------------------------------------------------- +# 2. COMPUTE ENGINE +# ----------------------------------------------------------------------------- +printf "${BOLD}${CYAN} [ COMPUTE ENGINE ]${NC} ${DIM}(VMs always-on = always costs)${NC}\n" +VMS=$(gcloud compute instances list \ + --project="${PROJECT_ID}" \ + --format="value(name,zone,status,machineType.basename())" 2>/dev/null || true) + +if [[ -z "$VMS" ]]; then + printf " ${OK} No VM instances\n" +else + ANY_RISK=true + while IFS=$'\t' read -r NAME ZONE STATUS MTYPE; do + if [[ "$STATUS" == "RUNNING" ]]; then + printf " ${FAIL} %-24s ${RED}RUNNING${NC} zone:%-20s type:%s\n" "$NAME" "$ZONE" "$MTYPE" + else + printf " ${WARN} %-24s ${YELLOW}%-8s${NC} zone:%-20s type:%s\n" "$NAME" "$STATUS" "$ZONE" "$MTYPE" + fi + done <<< "$VMS" +fi +echo "" + +# ----------------------------------------------------------------------------- +# 3. CLOUD SQL +# ----------------------------------------------------------------------------- +printf "${BOLD}${CYAN} [ CLOUD SQL ]${NC} ${DIM}(instances always-on when RUNNABLE)${NC}\n" +SQL=$(gcloud sql instances list \ + --project="${PROJECT_ID}" \ + --format="value(name,region,databaseVersion,settings.tier,state)" 2>/dev/null || true) + +if [[ -z "$SQL" ]]; then + printf " ${OK} No Cloud SQL instances\n" +else + ANY_RISK=true + while IFS=$'\t' read -r NAME SQLREGION DBVER TIER STATE; do + if [[ "$STATE" == "RUNNABLE" ]]; then + printf " ${FAIL} %-22s ${RED}RUNNABLE${NC} tier:%-14s db:%s\n" "$NAME" "$TIER" "$DBVER" + else + printf " ${WARN} %-22s ${YELLOW}%-10s${NC} tier:%-14s db:%s\n" "$NAME" "$STATE" "$TIER" "$DBVER" + fi + done <<< "$SQL" +fi +echo "" + +# ----------------------------------------------------------------------------- +# 4. CLOUD COMPOSER +# ----------------------------------------------------------------------------- +printf "${BOLD}${CYAN} [ CLOUD COMPOSER ]${NC} ${DIM}(managed Airflow = very expensive)${NC}\n" +COMP=$(gcloud composer environments list \ + --project="${PROJECT_ID}" \ + --locations="${REGION}" \ + --format="value(name,location,state)" 2>/dev/null || true) + +if [[ -z "$COMP" ]]; then + printf " ${OK} No Composer environments\n" +else + ANY_RISK=true + while IFS=$'\t' read -r NAME LOC STATE; do + printf " ${FAIL} %-24s ${RED}%-10s${NC} loc:%s\n" "$NAME" "$STATE" "$LOC" + done <<< "$COMP" +fi +echo "" + +# ----------------------------------------------------------------------------- +# 5. MEMORY BANK / REASONING ENGINES +# ----------------------------------------------------------------------------- +printf "${BOLD}${CYAN} [ MEMORY BANK ]${NC} ${DIM}(\$0.0994/vCPU-hr continuous billing)${NC}\n" +ENGINES=$(gcloud ai reasoning-engines list \ + --project="${PROJECT_ID}" \ + --region="${REGION}" \ + --format="value(name,displayName)" 2>/dev/null || true) + +if [[ -z "$ENGINES" ]]; then + printf " ${OK} No active Memory Bank instances\n" +else + ANY_RISK=true + ENGINE_COUNT=$(echo "$ENGINES" | grep -c . || true) + printf " ${FAIL} ${RED}${BOLD}${ENGINE_COUNT} INSTANCE(S) RUNNING — DELETE WHEN NOT NEEDED${NC}\n" + while IFS=$'\t' read -r ENAME EDISPLAY; do + SHORT=$(echo "$ENAME" | awk -F'/' '{print $NF}') + printf " ${WARN} %-22s ${DIM}id:%s${NC}\n" "${EDISPLAY:-unnamed}" "$SHORT" + printf " ${DIM} Delete: gcloud ai reasoning-engines delete %s --project=%s --region=%s --quiet${NC}\n" "$SHORT" "${PROJECT_ID}" "${REGION}" + done <<< "$ENGINES" +fi +echo "" + +# ----------------------------------------------------------------------------- +# 6. FIRESTORE +# ----------------------------------------------------------------------------- +printf "${BOLD}${CYAN} [ FIRESTORE ]${NC}\n" +gcloud firestore databases list \ + --project="${PROJECT_ID}" \ + --format="table[no-heading](name,type,locationId,freeTier)" 2>/dev/null | while read -r LINE; do + if echo "$LINE" | grep -q "True\|true"; then + printf " ${OK} %-40s ${DIM}(free tier)${NC}\n" "$LINE" + else + printf " ${WARN} %-40s ${YELLOW}(paid tier — watch usage)${NC}\n" "$LINE" + fi + done || printf " ${DOT} No Firestore databases\n" +echo "" + +# ----------------------------------------------------------------------------- +# 7. ARTIFACT REGISTRY +# ----------------------------------------------------------------------------- +printf "${BOLD}${CYAN} [ ARTIFACT REGISTRY ]${NC} ${DIM}(storage cost per GB)${NC}\n" +REPOS=$(gcloud artifacts repositories list \ + --project="${PROJECT_ID}" \ + --location="${REGION}" \ + --format="value(name.basename(),format,sizeBytes)" 2>/dev/null || true) + +if [[ -z "$REPOS" ]]; then + printf " ${OK} No Artifact Registry repos\n" +else + while IFS=$'\t' read -r NAME FMT SIZE; do + SIZE_MB=$(( ${SIZE:-0} / 1048576 )) + printf " ${OK} %-28s fmt:%-8s size:${SIZE_MB}MB\n" "$NAME" "$FMT" + done <<< "$REPOS" +fi +echo "" + +# ----------------------------------------------------------------------------- +# SUMMARY +# ----------------------------------------------------------------------------- +printf "${DIM} -------------------------------------------------------${NC}\n" +if [[ "$ANY_RISK" == true ]]; then + printf " ${WARN} ${YELLOW}${BOLD}ATTENTION: Billable resources detected above — review!${NC}\n" +else + printf " ${OK} ${GREEN}${BOLD}All clear — no unexpected idle resources detected.${NC}\n" +fi +printf " ${DIM}Full billing: https://console.cloud.google.com/billing?project=${PROJECT_ID}${NC}\n" +printf "${DIM} -------------------------------------------------------${NC}\n" +echo ""