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.
157 lines
5.4 KiB
Bash
Executable File
157 lines
5.4 KiB
Bash
Executable File
#!/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}'"
|