OSVauco/infrastructure/10-cost-guard.sh
Chris Christiansen 05030edf61 fix: Set executable bit on shell and python scripts
Per the task in `docs/TODO.md`, this change runs `git update-index --chmod=+x` on all relevant `.sh` and `.py` files in the repository.

This prevents intermittent 'Permission denied' errors when scripts are run in CI/CD environments or after being edited via the GitHub web interface, which can strip file permissions.
2026-05-26 13:23:18 +00:00

157 lines
5.4 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# 10-cost-guard.sh — Deploy Cloud Function som lytter på billing Pub/Sub
# og utfører auto-teardown + sender webhook-alert til chat-app
# ved 100% av budsjett.
#
# Forutsetter:
# - Pub/Sub topic 'billing-alert-auto-teardown' finnes (01-setupenv.sh)
# - Secret Manager secret 'webhook-url' inneholder chat-webhook URL
# gcloud secrets create webhook-url --data-file=<(echo -n "https://DIN-WEBHOOK-URL")
# Source .env før kjøring: source .env
set -euo pipefail
: "${PROJECT_ID:?Set PROJECT_ID}"
: "${REGION:?Set REGION}"
: "${CLOUD_RUN_SERVICE:?Set CLOUD_RUN_SERVICE}"
FUNCTION_NAME="billing-auto-teardown"
PUBSUB_TOPIC="billing-alert-auto-teardown"
FUNC_DIR="/tmp/cost-guard-fn"
echo "=== 10: Cost Guard — Cloud Function deployment ==="
bash "$(dirname "$0")/00-authcheck.sh"
gcloud services enable cloudfunctions.googleapis.com pubsub.googleapis.com --project="${PROJECT_ID}" --quiet
# --- Skriv Cloud Function kildekode ---
mkdir -p "${FUNC_DIR}"
cat > "${FUNC_DIR}/main.py" << 'PYEOF'
import base64
import json
import os
import subprocess
import urllib.request
import functions_framework
from google.cloud import secretmanager
def get_webhook_url(project_id: str) -> str | None:
try:
client = secretmanager.SecretManagerServiceClient()
name = f"projects/{project_id}/secrets/webhook-url/versions/latest"
response = client.access_secret_version(request={"name": name})
return response.payload.data.decode("UTF-8")
except Exception:
return None
def send_webhook(url: str, message: str) -> None:
payload = json.dumps({"text": message}).encode()
req = urllib.request.Request(
url,
data=payload,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=10):
pass
@functions_framework.cloud_event
def billing_alert(cloud_event):
project_id = os.environ.get("PROJECT_ID", "")
service = os.environ.get("CLOUD_RUN_SERVICE", "")
region = os.environ.get("REGION", "")
# Dekod Pub/Sub-melding
data = base64.b64decode(cloud_event.data["message"]["data"]).decode()
alert = json.loads(data)
budget_amount = alert.get("budgetAmount", "?")
cost_amount = alert.get("costAmount", "?")
alert_threshold = alert.get("alertThresholdExceeded", 0)
print(f"Billing alert: {cost_amount} / {budget_amount} ({alert_threshold*100:.0f}%)")
webhook_url = get_webhook_url(project_id)
base_msg = (
f"🚨 *OSVauco Cost Alert*\n"
f"Prosjekt: `{project_id}`\n"
f"Forbruk: `{cost_amount}` av budsjett `{budget_amount}`\n"
f"Terskel: `{alert_threshold*100:.0f}%`"
)
# Kjør auto-teardown kun ved 100% terskel
if alert_threshold >= 1.0:
print("100% terskel nådd — starter auto-teardown")
try:
subprocess.run(
[
"gcloud", "run", "services", "delete", service,
f"--region={region}",
f"--project={project_id}",
"--quiet",
],
check=True,
capture_output=True,
)
teardown_msg = base_msg + "\n\n Auto-teardown utført: Cloud Run stoppet."
except subprocess.CalledProcessError as e:
teardown_msg = base_msg + f"\n\n⚠ Auto-teardown FEILET: {e.stderr.decode()}"
msg = teardown_msg
else:
msg = base_msg + "\n\n⚠ Ingen tiltak nå — men pass på forbruket."
if webhook_url:
try:
send_webhook(webhook_url, msg)
print("Webhook-alert sendt")
except Exception as ex:
print(f"Webhook feilet: {ex}")
else:
print("Ingen webhook-url i Secret Manager — hopper over varsling")
PYEOF
cat > "${FUNC_DIR}/requirements.txt" << 'EOF'
functions-framework==3.*
google-cloud-secret-manager>=2.0.0
EOF
echo "✓ Cloud Function kildekode skrevet til ${FUNC_DIR}"
# --- Deploy Cloud Function ---
gcloud functions deploy "${FUNCTION_NAME}" \
--gen2 \
--runtime=python312 \
--region="${REGION}" \
--source="${FUNC_DIR}" \
--entry-point=billing_alert \
--trigger-topic="${PUBSUB_TOPIC}" \
--project="${PROJECT_ID}" \
--set-env-vars="PROJECT_ID=${PROJECT_ID},CLOUD_RUN_SERVICE=${CLOUD_RUN_SERVICE},REGION=${REGION}" \
--service-account="${AGENT_SA:-$(gcloud config get-value account)}" \
--quiet
echo "✓ Cloud Function deployed: ${FUNCTION_NAME}"
# --- IAM: Cloud Function SA trenger run.admin ---
PROJECT_NUMBER=$(gcloud projects describe "${PROJECT_ID}" --format="value(projectNumber)")
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
--member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
--role="roles/run.admin" --quiet
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
--member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor" --quiet
echo "✓ IAM: run.admin + secretmanager.secretAccessor gitt til Cloud Function SA"
echo ""
echo "=== 10: Cost Guard COMPLETE ==="
echo " Legg til webhook URL i Secret Manager:"
echo " gcloud secrets create webhook-url --data-file=<(echo -n \"https://DIN-WEBHOOK-URL\")"
echo " Kompatibel med: Google Chat, Slack, Discord, Teams"
echo " Tester: publiser en test-melding på Pub/Sub:"
echo " gcloud pubsub topics publish ${PUBSUB_TOPIC} --message='{\"budgetAmount\":75,\"costAmount\":75,\"alertThresholdExceeded\":1.0}'"