#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONFIG_PATH="${PROLE_DEPLOY_CFG:-${ROOT_DIR}/conf/gke.cfg}" if [[ -x "${ROOT_DIR}/.venv/bin/python3" ]]; then PYTHON_BIN="${ROOT_DIR}/.venv/bin/python3" elif [[ -x "${ROOT_DIR}/bin/python3" ]]; then PYTHON_BIN="${ROOT_DIR}/bin/python3" else PYTHON_BIN="${PYTHON:-python3}" fi # Validate StorageClass on GKE before deployment if [[ "$CONFIG_PATH" == *"gke.cfg"* ]]; then echo "Validating Supabase StorageClass configuration..." "${PYTHON_BIN}" - "${CONFIG_PATH}" <<'PY' import sys import subprocess import json import configparser def die(msg): print(f"ERROR: {msg}") sys.exit(1) c = configparser.ConfigParser() c.read(sys.argv[1]) g = c["Global"] if "Global" in c else {} e = c["env_setup"] if "env_setup" in c else {} i = c["init_cluster"] if "init_cluster" in c else {} ctx = g.get("APP_CLUSTER_KUBECONTEXT") or e.get("APP_CLUSTER_KUBECONTEXT") or i.get("app_cluster_kubecontext", "") if not ctx: sys.exit(0) # Resolve StorageClass name sc_name = g.get("SUPABASE_STORAGE_CLASS") or c.get("Supabase", "STORAGE_CLASS", fallback="supabase-standard") try: out = subprocess.check_output(["kubectl", "--context", ctx, "get", "storageclass", sc_name, "-o", "json"], stderr=subprocess.DEVNULL, text=True) sc = json.loads(out) provisioner = sc.get("provisioner", "") params = sc.get("parameters", {}) disk_type = params.get("type", "") if provisioner == "pd.csi.storage.gke.io": if disk_type in ["pd-ssd", "pd-balanced"]: die(f"storage-class configuration error: StorageClass '{sc_name}' is SSD-backed (type: {disk_type}). " "Supabase must use HDD-backed storage (pd-standard) on GKE to avoid quota issues.") elif disk_type != "pd-standard": die(f"storage-class configuration error: StorageClass '{sc_name}' has ambiguous disk type '{disk_type}'.") print(f"Verified Supabase StorageClass '{sc_name}' (type: {disk_type or 'unknown'}).") except subprocess.CalledProcessError: # If it's the very first deploy, the StorageClass might be missing because it's in the Helm chart we're about to apply. # However, if it's already missing and we're NOT on the first deploy, it might be an issue. # But usually, if it's in the chart, we shouldn't fail if it's missing *before* the first deploy. # But the requirement says "If the required HDD-backed StorageClass is missing, fail clearly." # Let's assume for now that if it's missing, we allow it to be created by the deploy, # UNLESS it's already there and wrong. print(f"Note: StorageClass '{sc_name}' not found; will be created by deployment.") PY fi "${PYTHON_BIN}" -m prole.deploy_pipeline --config "${CONFIG_PATH}" "$@" # Post-deploy summary echo "" echo "Public Endpoints:" "${PYTHON_BIN}" - "${CONFIG_PATH}" <<'PY' import sys import subprocess import json import configparser import time def get_config(path): c = configparser.ConfigParser() c.read(path) # Check [Global] then [env_setup] then [init_cluster] for contexts g = c["Global"] if "Global" in c else {} e = c["env_setup"] if "env_setup" in c else {} i = c["init_cluster"] if "init_cluster" in c else {} app_ctx = g.get("APP_CLUSTER_KUBECONTEXT") or e.get("APP_CLUSTER_KUBECONTEXT") or i.get("app_cluster_kubecontext", "") db_ctx = g.get("DB_CLUSTER_KUBECONTEXT") or e.get("DB_CLUSTER_KUBECONTEXT") or i.get("db_cluster_kubecontext", "") return app_ctx, db_ctx def get_ip(host, ctx): if not ctx: return "pending" # Try Ingress first try: out = subprocess.check_output(["kubectl", "--context", ctx, "get", "ingress", "-A", "-o", "json"], stderr=subprocess.DEVNULL, text=True) items = json.loads(out).get("items", []) for item in items: for rule in item.get("spec", {}).get("rules", []): if rule.get("host") == host: ing = item.get("status", {}).get("loadBalancer", {}).get("ingress", []) if ing: val = ing[0].get("ip") or ing[0].get("hostname") if val: return val except: pass # Public APP endpoints must be sourced from Ingress status # api.knoe.dev, git.knoe.dev, svc.knoe.dev, api.0.knoe.dev, db.0.knoe.dev return "pending" def check_supabase_pvcs(ctx): if not ctx: return None try: out = subprocess.check_output(["kubectl", "--context", ctx, "get", "events", "-n", "supabase", "-o", "json"], stderr=subprocess.DEVNULL, text=True) events = json.loads(out).get("items", []) blocking_pvcs = {} for event in events: obj = event.get("involvedObject", {}) if obj.get("kind") == "PersistentVolumeClaim": msg = event.get("message", "") reason = event.get("reason", "") if "QUOTA_EXCEEDED" in msg or "ProvisioningFailed" in reason: if "exceeded" in msg.lower() or "quota" in msg.lower(): pvc_name = obj.get("name") blocking_pvcs[pvc_name] = msg if blocking_pvcs: return blocking_pvcs except: pass return None app_ctx, db_ctx = get_config(sys.argv[1]) # Targets we want to wait for if they are pending wait_hosts = ["api.0.knoe.dev", "db.0.knoe.dev"] timeout = 300 # 5 minutes start = time.time() while time.time() - start < timeout: pvc_errors = check_supabase_pvcs(app_ctx) if pvc_errors: print("\nERROR: Supabase PVC provisioning failed (Storage Quota Exceeded).") for pvc, msg in pvc_errors.items(): print(f" - PVC: {pvc}") print(f" GKE Error: {msg}") sys.exit(1) pending = False for h in wait_hosts: if get_ip(h, app_ctx) == "pending": pending = True break if not pending: break time.sleep(10) if time.time() - start >= timeout: print("\nERROR: Timeout waiting for Supabase ingress reconciliation.") print("Ingress Controller Diagnosis (kubectl get events -n supabase):") try: subprocess.run(["kubectl", "--context", app_ctx, "get", "events", "-n", "supabase", "--sort-by=.lastTimestamp"], check=False) print("\nIngress Resource Status:") subprocess.run(["kubectl", "--context", app_ctx, "describe", "ingress", "supabase-kong", "-n", "supabase"], check=False) subprocess.run(["kubectl", "--context", app_ctx, "describe", "ingress", "supabase-studio", "-n", "supabase"], check=False) except: pass sys.exit(1) endpoints = [ ("api.knoe.dev", "knoe-svc-kong", "APP", app_ctx), ("git.knoe.dev", "GitLab", "APP", app_ctx), ("db.0.knoe.dev", "Supabase Studio", "APP", app_ctx), ("api.0.knoe.dev", "Supabase Kong", "APP", app_ctx), ("svc.knoe.dev", "Grafana", "APP", app_ctx), ] print(f"{'hostname':<20} {'service':<20} {'cluster':<10} {'external IP':<15}") print("-" * 70) for host, svc, clus, ctx in endpoints: ip = get_ip(host, ctx) print(f"{host:<20} {svc:<20} {clus:<10} {ip:<15}") PY