#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" if [[ -f "$PROJECT_ROOT/env.sh" ]]; then # shellcheck disable=SC1090 source "$PROJECT_ROOT/env.sh" fi MODE="k3d" HELM_RELEASE="supabase" USE_DEV_HELPERS="false" FOREGROUND="false" FORCE="true" PROLE_CFG_PATH="" HELM_TEMPLATE_ONLY="false" PREFETCH_IMAGES_ONLY="false" SKIP_PREFETCH="false" usage() { cat <<'USAGE' supabase/deploy.sh Launches the full Supabase open-source stack locally, following the README/DEVELOPERS guidance (Docker Compose). This is a multi-service deployment (Postgres, Auth, Storage, Realtime, Kong, Studio, etc.) Usage: ./deploy.sh [options] Options: --mode Deployment mode ('local' for Docker Compose, 'k3d' for local k3d, 'k8s' for generic k8s/k3s; default: k3d) --helm-template-only Render Helm manifests to supabase/k8s without applying (for pipelines) --prefetch-images-only Discover/prepare/load/import Supabase images only, then exit (k3d mode) --skip-prefetch Skip image prefetch during deploy (for preloaded flows) --with-dev-helpers Include docker/dev/docker-compose.dev.yml --foreground Run docker compose in the foreground (default: detached, local mode only) -f, --force Reset the Supabase namespace before applying manifests (k3d only; default) --no-force Skip namespace reset (k3d only) -c, --config Path to prole.cfg (loads environment defaults) -h, --help Show help Notes: - This script syncs the Supabase repo into $DEV_HOME/supabase. - The deployment runs as a single "supabase" namespace via the Compose project name. - For k3d mode, manifests are applied from $SUPABASE_K8S_DIR. - Docker image artifacts are cached in $DOCKER_IMPORT_DIR. - This script preserves the README/DEVELOPERS steps: 1) use docker/docker-compose.yml 2) copy docker/.env.example -> docker/.env (if missing) 3) run docker compose up USAGE } die() { echo "Error: $*" >&2 exit 1 } warn() { echo "Warning: $*" >&2 } log() { echo "==> $*" } apply_defaults() { DEV_HOME="${DEV_HOME:-$HOME/dev}" DEV_HOME="${DEV_HOME/#\~/$HOME}" SUPABASE_DIR="$DEV_HOME/supabase" DOCKER_DIR="$SUPABASE_DIR/docker" COMPOSE_FILE="$DOCKER_DIR/docker-compose.yml" DEV_COMPOSE_FILE="$DOCKER_DIR/dev/docker-compose.dev.yml" ENV_EXAMPLE="$DOCKER_DIR/.env.example" ENV_FILE="$DOCKER_DIR/.env" SUPABASE_K8S_DIR="${SUPABASE_K8S_DIR:-$SCRIPT_DIR/../build/supabase-k8s}" local default_owner default_owner="$(id -un 2>/dev/null || echo prole)" local data_root="${PROLE_DATA:-/opt/prole/data/${default_owner}}" data_root="${data_root%/}" DOCKER_IMPORT_DIR="${DOCKER_IMPORT_DIR:-$data_root/docker-import}" SUPABASE_IMAGE_PLATFORM="${SUPABASE_IMAGE_PLATFORM:-}" SUPABASE_IMAGE_PLATFORMS="${SUPABASE_IMAGE_PLATFORMS:-linux/amd64 linux/arm64}" SUPABASE_POSTGRES_PORT="${SUPABASE_POSTGRES_PORT:-15432}" PROLE_DB_SERVICE="${PROLE_DB_SERVICE:-prole-db-rw}" PROLE_DB_NAMESPACE="${PROLE_DB_NAMESPACE:-}" } load_prole_cfg() { local cfg_loader="$PROJECT_ROOT/etc/prole_cfg.sh" local cfg_path="${PROLE_CFG_PATH:-}" if [[ -z "$cfg_path" ]]; then if [[ -n "${PROLE_CONF:-}" ]]; then cfg_path="${PROLE_CONF%/}/prole.cfg" elif [[ -f "$PROJECT_ROOT/conf/prole.cfg" ]]; then cfg_path="$PROJECT_ROOT/conf/prole.cfg" fi fi if [[ -z "$cfg_path" ]]; then return 0 fi if [[ -n "$cfg_path" ]]; then if [[ -d "$cfg_path" ]]; then cfg_path="$cfg_path/prole.cfg" fi [[ -f "$cfg_path" ]] || die "Config file not found: $cfg_path" PROLE_CONF="$(cd "$(dirname "$cfg_path")" && pwd)" export PROLE_CONF PROLE_CFG_PATH="$cfg_path" fi if [[ -f "$cfg_loader" ]]; then # shellcheck disable=SC1090 source "$cfg_loader" else warn "prole_cfg.sh not found; config defaults may be incomplete." fi } register_supabase_ports() { # Register port forwards if helper is available if command -v prole_register_port_forward >/dev/null 2>&1; then prole_register_port_forward "supabase-kong" "supabase" "svc/kong" "8000" "8000" "0.0.0.0" "TCP" "Supabase API (Kong)" prole_register_port_forward "supabase-studio" "supabase" "svc/studio" "8082" "3000" "0.0.0.0" "TCP" "Supabase Studio" fi } require_cmd() { command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1" } detect_platform() { case "$(uname -m 2>/dev/null || true)" in arm64|aarch64) echo "linux/arm64" ;; x86_64|amd64) echo "linux/amd64" ;; *) echo "linux/amd64" ;; esac } normalize_platform() { case "${1:-}" in linux/*) echo "$1" ;; arm64|aarch64) echo "linux/arm64" ;; x86_64|amd64) echo "linux/amd64" ;; "") detect_platform ;; *) detect_platform ;; esac } compose_cmd() { if docker compose version >/dev/null 2>&1; then echo "docker compose" return 0 fi if command -v docker-compose >/dev/null 2>&1; then echo "docker-compose" return 0 fi die "Docker Compose not found (expected 'docker compose' or 'docker-compose')" } ensure_k3d() { require_cmd k3d } ensure_kompose() { require_cmd kompose } ensure_helm() { require_cmd helm } list_k3d_clusters() { local json="" if json="$(k3d cluster list -o json 2>/dev/null)"; then if [[ -n "$json" ]]; then if printf '%s' "$json" | python - <<'PY' import json import sys raw = sys.stdin.read() if not raw.strip(): sys.exit(1) try: data = json.loads(raw) except Exception: sys.exit(1) items = data.get("items") if isinstance(data, dict) else data if not items: sys.exit(0) for item in items: name = item.get("name") if isinstance(item, dict) else None if name: print(name) PY then return 0 fi fi fi k3d cluster list 2>/dev/null | awk 'NR>1 {print $1}' } cluster_exists() { local name="$1" list_k3d_clusters | awk -v target="$name" '$0 == target {found=1} END {exit found ? 0 : 1}' } ensure_k3d_cluster() { local cluster="${K3D_CLUSTER_NAME:-}" local -a clusters local picked="" mapfile -t clusters < <(list_k3d_clusters || true) if [[ -n "$cluster" ]]; then if ! cluster_exists "$cluster"; then log "Creating k3d cluster '$cluster'..." k3d cluster create "$cluster" >/dev/null fi picked="$cluster" elif [[ ${#clusters[@]} -gt 0 ]]; then for name in "${clusters[@]}"; do if [[ "$name" == "k3s-default" ]]; then picked="$name" break fi done if [[ -z "$picked" ]]; then picked="${clusters[0]}" fi else picked="k3s-default" log "Creating k3d cluster '$picked'..." k3d cluster create "$picked" >/dev/null fi export K3D_CLUSTER_NAME="$picked" log "Using k3d cluster: $K3D_CLUSTER_NAME" k3d cluster start "$K3D_CLUSTER_NAME" >/dev/null 2>&1 || true k3d kubeconfig merge "$K3D_CLUSTER_NAME" --switch-context >/dev/null 2>&1 || true } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --mode|-m) MODE="${2:-}" shift 2 ;; -c|--config) PROLE_CFG_PATH="${2:-}" [[ -n "$PROLE_CFG_PATH" ]] || die "Missing value for $1" shift 2 ;; --config=*) PROLE_CFG_PATH="${1#*=}" [[ -n "$PROLE_CFG_PATH" ]] || die "Missing value for $1" shift ;; --with-dev-helpers) USE_DEV_HELPERS="true" shift ;; --helm-template-only) HELM_TEMPLATE_ONLY="true" shift ;; --prefetch-images-only) PREFETCH_IMAGES_ONLY="true" shift ;; --skip-prefetch) SKIP_PREFETCH="true" shift ;; --foreground) FOREGROUND="true" shift ;; -f|--force) FORCE="true" shift ;; --no-force) FORCE="false" shift ;; -h|--help) usage exit 0 ;; *) die "Unknown option: $1" ;; esac done } ensure_env_file() { if [[ -f "$ENV_FILE" ]]; then return 0 fi [[ -f "$ENV_EXAMPLE" ]] || die "Missing env example file: $ENV_EXAMPLE" log "Creating docker/.env from docker/.env.example" cp "$ENV_EXAMPLE" "$ENV_FILE" warn "docker/.env contains default secrets. Update them before any production use." } compose_files() { local -a files=("-f" "docker-compose.yml") if [[ "$USE_DEV_HELPERS" == "true" ]]; then [[ -f "$DEV_COMPOSE_FILE" ]] || die "Missing dev helpers compose file: $DEV_COMPOSE_FILE" files+=("-f" "dev/docker-compose.dev.yml") fi echo "${files[@]}" } ensure_repo() { require_cmd git mkdir -p "$DEV_HOME" if [[ -d "$SUPABASE_DIR/.git" ]]; then log "Updating Supabase repo in $SUPABASE_DIR" (cd "$SUPABASE_DIR" && git pull) return 0 fi if [[ -e "$SUPABASE_DIR" ]]; then die "Path exists but is not a git repo: $SUPABASE_DIR" fi log "Cloning Supabase repo into $SUPABASE_DIR" git clone https://github.com/supabase/supabase.git "$SUPABASE_DIR" } run_local() { ensure_repo [[ -f "$COMPOSE_FILE" ]] || die "Missing compose file: $COMPOSE_FILE" require_cmd docker docker info >/dev/null 2>&1 || die "Docker daemon is not running" local compose compose="$(compose_cmd)" log "Prole::Supabase local deploy (Docker Compose)" log "Repo: $SUPABASE_DIR" log "This follows README/DEVELOPERS guidance for running the full open-source stack." log "Namespace: supabase (Compose project name)" ensure_env_file local compose_args=("-f" "docker-compose.yml") if [[ "$USE_DEV_HELPERS" == "true" ]]; then [[ -f "$DEV_COMPOSE_FILE" ]] || die "Missing dev helpers compose file: $DEV_COMPOSE_FILE" compose_args+=("-f" "dev/docker-compose.dev.yml") fi local up_args=("up") if [[ "$FOREGROUND" != "true" ]]; then up_args+=("-d") fi log "Starting Supabase services..." (cd "$DOCKER_DIR" && COMPOSE_PROJECT_NAME="supabase" $compose "${compose_args[@]}" "${up_args[@]}") log "Deployment complete." log "Access Studio at http://localhost:8082 (see docker/.env for ports)." } image_safe_name() { echo "$1" | sed 's/[\/:@]/_/g' } image_platform_tag() { local img="$1" local platform="$2" local suffix="${platform//\//-}" local name tag if [[ "$img" == *@* ]]; then name="${img%@*}" tag="digest-${suffix}" elif [[ "$img" == *:* ]]; then name="${img%:*}" tag="${img##*:}-${suffix}" else name="$img" tag="latest-${suffix}" fi echo "${name}:${tag}" } ensure_image_artifact() { local img="$1" local platform="$2" local safe_name local tar_path local platform_tag safe_name="$(image_safe_name "$img")" tar_path="${DOCKER_IMPORT_DIR}/${safe_name}.${platform//\//-}.tar" platform_tag="$(image_platform_tag "$img" "$platform")" if [[ -f "$tar_path" ]]; then return 0 fi mkdir -p "$DOCKER_IMPORT_DIR" # Prefer buildx export to reliably produce single-arch tarballs with containerd-backed Docker if docker buildx version >/dev/null 2>&1; then log "Fetching image $img for platform $platform (buildx export)" if ! printf 'FROM %s\n' "$img" \ | docker buildx build --pull --platform "$platform" -t "$platform_tag" \ --output "type=docker,dest=$tar_path" - >/dev/null 2>&1; then die "Failed to export image $img ($platform) via buildx" fi return 0 fi # Fallback: traditional pull + save (may fail on some Docker/containerd combos) log "Fetching image $img for platform $platform" if ! docker pull --platform "$platform" "$img" >/dev/null 2>&1; then if docker image inspect "$img" >/dev/null 2>&1; then warn "Using local image for $img (pull failed for $platform)" else die "Failed to pull image for $img ($platform)" fi fi docker tag "$img" "$platform_tag" >/dev/null 2>&1 || true log "Saving $img ($platform) to $tar_path" if ! docker save -o "$tar_path" "$platform_tag" >/dev/null 2>&1; then die "Failed to save image $img ($platform) to $tar_path" fi } load_image_for_platform() { local img="$1" local platform="$2" local safe_name local tar_path local platform_tag safe_name="$(image_safe_name "$img")" tar_path="${DOCKER_IMPORT_DIR}/${safe_name}.${platform//\//-}.tar" platform_tag="$(image_platform_tag "$img" "$platform")" if [[ ! -f "$tar_path" ]]; then ensure_image_artifact "$img" "$platform" fi log "Loading $img ($platform) from $tar_path" docker load -i "$tar_path" 2>&1 | sed 's/^/ /' docker tag "$platform_tag" "$img" } # Returns 0 if the image is already present in k3d's containerd store, 1 otherwise. image_in_k3d() { local img="$1" local cluster="${K3D_CLUSTER_NAME:-}" local node="k3d-${cluster}-server-0" if ! docker inspect "$node" >/dev/null 2>&1; then return 1 fi docker exec "$node" crictl images --no-trunc -o json 2>/dev/null \ | python3 - "$img" <<'PY' import json, sys target = sys.argv[1] try: data = json.load(sys.stdin) except Exception: sys.exit(1) for entry in data.get('images', []): for tag in entry.get('repoTags', []): if tag == target: sys.exit(0) sys.exit(1) PY } # Build the list of images already present in k3d's containerd store. list_k3d_images() { local cluster="${K3D_CLUSTER_NAME:-}" local node="k3d-${cluster}-server-0" if ! docker inspect "$node" >/dev/null 2>&1; then return 0 fi docker exec "$node" crictl images --no-trunc -o json 2>/dev/null \ | python3 - <<'PY' import json, sys try: data = json.load(sys.stdin) except Exception: sys.exit(0) for entry in data.get('images', []): for tag in entry.get('repoTags', []): print(tag) PY } write_artifact_manifest() { local images="$1" local list_path="${DOCKER_IMPORT_DIR}/supabase-images.txt" mkdir -p "$DOCKER_IMPORT_DIR" printf "%s\n" $images > "$list_path" local platform for platform in $SUPABASE_IMAGE_PLATFORMS; do local platform_list="${DOCKER_IMPORT_DIR}/supabase-images-${platform//\//-}.txt" : > "$platform_list" for img in $images; do local safe_name safe_name="$(image_safe_name "$img")" echo "${DOCKER_IMPORT_DIR}/${safe_name}.${platform//\//-}.tar" >> "$platform_list" done done } prefetch_k3d_images() { require_cmd docker docker info >/dev/null 2>&1 || die "Docker daemon is not running" local compose compose="$(compose_cmd)" ensure_env_file local files files=($(compose_files)) local images images=$(cd "$DOCKER_DIR" && $compose "${files[@]}" --env-file ".env" config --images | awk 'NF' | sort -u) if [[ -z "$images" ]]; then die "No images found in Supabase compose config" fi log "Supabase images discovered:" printf " - %s\n" $images write_artifact_manifest "$images" log "Image artifact list written to $DOCKER_IMPORT_DIR/supabase-images.txt" local platform for platform in $SUPABASE_IMAGE_PLATFORMS; do log "Ensuring artifacts for platform '$platform' in $DOCKER_IMPORT_DIR" for img in $images; do ensure_image_artifact "$img" "$platform" done done local deploy_platform deploy_platform="$(normalize_platform "${SUPABASE_IMAGE_PLATFORM:-}")" log "Loading images for platform '$deploy_platform'" for img in $images; do load_image_for_platform "$img" "$deploy_platform" done # Build a list of images already present in k3d to avoid re-importing them. log "Checking k3d registry for already-imported images..." local k3d_existing k3d_existing="$(list_k3d_images || true)" local -a import_delta=() local -a skipped=() for img in $images; do if printf '%s\n' "$k3d_existing" | grep -qxF "$img"; then skipped+=("$img") else import_delta+=("$img") fi done if [[ ${#skipped[@]} -gt 0 ]]; then log "Already in k3d registry (skipping ${#skipped[@]} image(s)):" printf " [skip] %s\n" "${skipped[@]}" fi if [[ ${#import_delta[@]} -eq 0 ]]; then log "All images already present in k3d registry; nothing to import." return 0 fi log "Importing ${#import_delta[@]} image(s) into k3d cluster '${K3D_CLUSTER_NAME:-}' (delta only)..." local idx=0 for img in "${import_delta[@]}"; do idx=$(( idx + 1 )) log " [${idx}/${#import_delta[@]}] Importing $img into k3d..." if [[ -n "${K3D_CLUSTER_NAME:-}" ]]; then k3d image import "$img" -c "$K3D_CLUSTER_NAME" else k3d image import "$img" fi log " [${idx}/${#import_delta[@]}] Done: $img" done log "k3d image import complete (${#import_delta[@]} imported, ${#skipped[@]} already present)." } resolve_prole_db_namespace() { if [[ -n "${PROLE_DB_NAMESPACE:-}" ]]; then return 0 fi if [[ -n "${NAMESPACE:-}" ]]; then PROLE_DB_NAMESPACE="$NAMESPACE" return 0 fi if kubectl get namespace prole >/dev/null 2>&1; then PROLE_DB_NAMESPACE="prole" else PROLE_DB_NAMESPACE="default" fi } patch_db_deployment_port() { local file="$SUPABASE_K8S_DIR/db-deployment.yaml" if [[ ! -f "$file" ]]; then return 0 fi local port="$SUPABASE_POSTGRES_PORT" python - "$file" "$port" <<'PY' import re import sys path = sys.argv[1] port = sys.argv[2] with open(path, "r", encoding="utf-8") as fh: data = fh.read() data = re.sub(r"(name:\s*PGPORT\s*\n\s*value:\s*)\"[^\"]+\"", rf"\1\"{port}\"", data) data = re.sub(r"(name:\s*POSTGRES_PORT\s*\n\s*value:\s*)\"[^\"]+\"", rf"\1\"{port}\"", data) with open(path, "w", encoding="utf-8") as fh: fh.write(data) PY } write_supabase_postgres_service() { local file="$SUPABASE_K8S_DIR/supabase-postgres-service.yaml" cat > "$file" < "$file" < emptyDir (postgres needs writable data dir) local db_deploy="$SUPABASE_K8S_DIR/db-deployment.yaml" if [[ -f "$db_deploy" ]]; then python3 - "$db_deploy" <<'PYFIX' import sys, re path = sys.argv[1] with open(path, 'r') as f: content = f.read() # Replace the db-cm4 configMap volume definition with emptyDir # Matches: - configMap:\n name: db-cm4\n name: db-cm4 content = re.sub( r'(\s+)-\s+configMap:\s*\n\s+name:\s*db-cm4\s*\n(\s+name:\s*db-cm4)', r'\1- emptyDir: {}\n\2', content ) # Also handle the alternate format kompose might produce content = re.sub( r'(\s+)-\s+configMap:\s*\n\s+defaultMode:\s*\d+\s*\n\s+name:\s*db-cm4\s*\n(\s+name:\s*db-cm4)', r'\1- emptyDir: {}\n\2', content ) with open(path, 'w') as f: f.write(content) print(f" [OK] db data volume -> emptyDir") PYFIX fi # 2. Remove db-cm4 configmap (no longer needed; postgres inits its own data dir) local db_cm4="$SUPABASE_K8S_DIR/db-cm4-configmap.yaml" if [[ -f "$db_cm4" ]]; then rm -f "$db_cm4" log " Removed db-cm4-configmap.yaml (data dir handled by emptyDir)" fi # 3. Scale vector to 0 replicas (docker_logs source needs docker.sock, unavailable in k8s) local vector_deploy="$SUPABASE_K8S_DIR/vector-deployment.yaml" if [[ -f "$vector_deploy" ]]; then python3 - "$vector_deploy" <<'PYFIX' import sys, re path = sys.argv[1] with open(path, 'r') as f: content = f.read() content = re.sub(r'(spec:\s*\n\s+replicas:)\s*\d+', r'\1 0', content, count=1) with open(path, 'w') as f: f.write(content) print(" [OK] vector replicas -> 0 (docker_logs incompatible with k8s)") PYFIX fi # 4. Scale functions to 0 replicas (edge-runtime needs function files not present in k8s) local functions_deploy="$SUPABASE_K8S_DIR/functions-deployment.yaml" if [[ -f "$functions_deploy" ]]; then python3 - "$functions_deploy" <<'PYFIX' import sys, re path = sys.argv[1] with open(path, 'r') as f: content = f.read() content = re.sub(r'(spec:\s*\n\s+replicas:)\s*\d+', r'\1 0', content, count=1) with open(path, 'w') as f: f.write(content) print(" [OK] functions replicas -> 0 (edge-runtime entrypoint not available in k8s)") PYFIX fi # 6. Scale db deployment to 0 replicas (using prole-db instead of supabase standalone postgres) if [[ -f "$db_deploy" ]]; then python3 - "$db_deploy" <<'PYFIX' import sys, re path = sys.argv[1] with open(path, 'r') as f: content = f.read() content = re.sub(r'(spec:\s*\n\s+replicas:)\s*\d+', r'\1 0', content, count=1) with open(path, 'w') as f: f.write(content) print(" [OK] db replicas -> 0 (using prole-db as database backend)") PYFIX fi # 7. Fix liveness probes that reference Docker Compose service hostnames # In Docker Compose, service names resolve via internal DNS, but in k8s # there is no matching Service for every container. Rewrite probes to # use localhost (the probe checks the container's own health endpoint). local storage_deploy="$SUPABASE_K8S_DIR/storage-deployment.yaml" if [[ -f "$storage_deploy" ]]; then python3 - "$storage_deploy" <<'PYFIX' import sys path = sys.argv[1] with open(path, 'r') as f: content = f.read() content = content.replace('http://storage:5000/status', 'http://localhost:5000/status') with open(path, 'w') as f: f.write(content) print(" [OK] storage liveness probe -> localhost:5000 (no k8s Service needed)") PYFIX fi # 8. Fix studio liveness probe: studio:3000 -> localhost:3000 local studio_deploy="$SUPABASE_K8S_DIR/studio-deployment.yaml" if [[ -f "$studio_deploy" ]]; then python3 - "$studio_deploy" <<'PYFIX' import sys path = sys.argv[1] with open(path, 'r') as f: content = f.read() content = content.replace('http://studio:3000/', 'http://localhost:3000/') with open(path, 'w') as f: f.write(content) print(" [OK] studio liveness probe -> localhost:3000") PYFIX fi # 9. Fix realtime liveness probe: kompose emits the whole curl command as a # single argv entry, and unquoted `Authorization: ...` fragments can be # parsed as YAML mappings. Sanitize the probe so Kubernetes receives a # proper list of strings. local realtime_deploy="$SUPABASE_K8S_DIR/realtime-deployment.yaml" if [[ -f "$realtime_deploy" ]]; then python3 "$SCRIPT_DIR/patch_realtime_probe.py" "$realtime_deploy" fi } patch_supabase_credentials() { log "Patching placeholder credentials in Supabase manifests..." local pg_password="" local jwt_secret="" # Resolve postgres password from prole-db-superuser secret resolve_prole_db_namespace local ns="${PROLE_DB_NAMESPACE:-${NAMESPACE:-default}}" if kubectl get secret prole-db-superuser -n "$ns" >/dev/null 2>&1; then pg_password=$(kubectl get secret prole-db-superuser -n "$ns" \ -o jsonpath='{.data.password}' 2>/dev/null | base64 -d 2>/dev/null || true) fi if [[ -z "$pg_password" ]]; then pg_password="${POSTGRES_PASSWORD:-}" fi # Resolve JWT secret from supabase-jwt secret or env if kubectl get secret supabase-jwt -n supabase >/dev/null 2>&1; then jwt_secret=$(kubectl get secret supabase-jwt -n supabase \ -o jsonpath='{.data.jwt-secret}' 2>/dev/null | base64 -d 2>/dev/null || true) fi if [[ -z "$jwt_secret" ]]; then local env_file="${ENV_FILE:-}" if [[ -f "$env_file" ]]; then jwt_secret=$(grep -E '^JWT_SECRET=' "$env_file" | head -1 | cut -d= -f2- | tr -d "'\"" || true) fi fi if [[ -z "$jwt_secret" ]]; then jwt_secret="${JWT_SECRET:-}" fi if [[ -z "$pg_password" ]]; then warn "Could not resolve postgres password; manifests will keep placeholder credentials." warn "Set POSTGRES_PASSWORD or ensure prole-db-superuser secret exists in namespace '$ns'." return 0 fi # Replace placeholder values in all deployment manifests local placeholder_pw="your-super-secret-and-long-postgres-password" local placeholder_jwt="your-super-secret-jwt-token-with-at-least-32-characters-long" local f for f in "$SUPABASE_K8S_DIR"/*-deployment.yaml; do [[ -f "$f" ]] || continue python3 - "$f" "$pg_password" "$jwt_secret" "$placeholder_pw" "$placeholder_jwt" <<'PYFIX' import sys path = sys.argv[1] pg_pw = sys.argv[2] jwt_sec = sys.argv[3] ph_pw = sys.argv[4] ph_jwt = sys.argv[5] with open(path, 'r') as fh: content = fh.read() changed = False if ph_pw in content and pg_pw: content = content.replace(ph_pw, pg_pw) changed = True if ph_jwt in content and jwt_sec: content = content.replace(ph_jwt, jwt_sec) changed = True if changed: with open(path, 'w') as fh: fh.write(content) PYFIX done log " Credentials patched in Supabase manifests." } setup_prole_db_for_supabase() { log "Ensuring Supabase roles, schemas and databases exist in prole-db..." resolve_prole_db_namespace local ns="${PROLE_DB_NAMESPACE:-${NAMESPACE:-default}}" # Find the primary CNPG pod local primary primary=$(kubectl -n "$ns" get pods \ -l "cnpg.io/cluster=prole-db,cnpg.io/instanceRole=primary" \ -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) if [[ -z "$primary" ]]; then primary=$(kubectl -n "$ns" get pods -l "cnpg.io/cluster=prole-db" \ -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) fi if [[ -z "$primary" ]]; then warn "No prole-db pod found in namespace '$ns'; skipping Supabase DB setup." return 0 fi local pg_password="" if kubectl get secret prole-db-superuser -n "$ns" >/dev/null 2>&1; then pg_password=$(kubectl get secret prole-db-superuser -n "$ns" \ -o jsonpath='{.data.password}' 2>/dev/null | base64 -d 2>/dev/null || true) fi if [[ -z "$pg_password" ]]; then pg_password="${POSTGRES_PASSWORD:-}" fi if [[ -z "$pg_password" ]]; then warn "Cannot resolve postgres password; skipping Supabase DB role setup." return 0 fi # Create roles required by Supabase services kubectl -n "$ns" exec "$primary" -c postgres -- psql -U postgres -d postgres -c " -- Core roles DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='anon') THEN CREATE ROLE anon NOLOGIN; END IF; END \$\$; DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='authenticated') THEN CREATE ROLE authenticated NOLOGIN; END IF; END \$\$; DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='service_role') THEN CREATE ROLE service_role NOLOGIN; END IF; END \$\$; DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='authenticator') THEN CREATE ROLE authenticator LOGIN PASSWORD '${pg_password}'; END IF; END \$\$; DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='pgbouncer') THEN CREATE ROLE pgbouncer LOGIN PASSWORD '${pg_password}'; END IF; END \$\$; DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='supabase_admin') THEN CREATE ROLE supabase_admin LOGIN PASSWORD '${pg_password}' SUPERUSER; END IF; END \$\$; DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='supabase_auth_admin') THEN CREATE ROLE supabase_auth_admin LOGIN PASSWORD '${pg_password}' NOINHERIT; END IF; END \$\$; DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='supabase_storage_admin') THEN CREATE ROLE supabase_storage_admin LOGIN PASSWORD '${pg_password}' NOINHERIT; END IF; END \$\$; DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='supabase_functions_admin') THEN CREATE ROLE supabase_functions_admin LOGIN PASSWORD '${pg_password}' NOINHERIT; END IF; END \$\$; DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='supabase_read_only_user') THEN CREATE ROLE supabase_read_only_user NOLOGIN; END IF; END \$\$; -- Grant membership GRANT anon TO authenticator; GRANT authenticated TO authenticator; GRANT service_role TO authenticator; GRANT supabase_admin TO authenticator; -- Schemas on postgres database CREATE SCHEMA IF NOT EXISTS auth AUTHORIZATION supabase_auth_admin; CREATE SCHEMA IF NOT EXISTS storage AUTHORIZATION supabase_storage_admin; CREATE SCHEMA IF NOT EXISTS graphql_public; CREATE SCHEMA IF NOT EXISTS _realtime; ALTER SCHEMA _realtime OWNER TO postgres; GRANT USAGE ON SCHEMA public TO anon, authenticated, service_role; GRANT USAGE ON SCHEMA auth TO anon, authenticated, service_role; GRANT USAGE ON SCHEMA storage TO anon, authenticated, service_role; GRANT USAGE ON SCHEMA graphql_public TO anon, authenticated, service_role; " 2>&1 || warn "Could not set up Supabase roles (cluster may not be ready yet)." # Create _supabase database (used by supavisor, analytics) local db_exists db_exists=$(kubectl -n "$ns" exec "$primary" -c postgres -- psql -U postgres -d postgres -tAc \ "SELECT 1 FROM pg_database WHERE datname = '_supabase';" 2>/dev/null || true) if [[ "$db_exists" != "1" ]]; then kubectl -n "$ns" exec "$primary" -c postgres -- psql -U postgres -d postgres -c \ "CREATE DATABASE _supabase OWNER postgres;" 2>&1 || warn "Could not create _supabase database." fi # Create schemas in _supabase database kubectl -n "$ns" exec "$primary" -c postgres -- psql -U postgres -d _supabase -c " CREATE SCHEMA IF NOT EXISTS _supavisor; ALTER SCHEMA _supavisor OWNER TO postgres; CREATE SCHEMA IF NOT EXISTS _analytics; ALTER SCHEMA _analytics OWNER TO postgres; " 2>&1 || warn "Could not create _supabase schemas." log " Supabase database roles and schemas ready." } generate_k8s_manifests() { ensure_kompose ensure_env_file local files files=($(compose_files)) rm -rf "$SUPABASE_K8S_DIR" mkdir -p "$SUPABASE_K8S_DIR" log "Generating Kubernetes manifests from docker-compose.yml" (cd "$DOCKER_DIR" && kompose "${files[@]}" -n supabase -o "$SUPABASE_K8S_DIR" --volumes=configMap --suppress-warnings convert) fix_k8s_manifests resolve_prole_db_namespace write_db_alias_service patch_db_deployment_port write_supabase_postgres_service patch_supabase_credentials sanitize_container_names } sanitize_container_names() { log "Sanitizing container names in manifests..." local files files=("$SUPABASE_K8S_DIR"/*-deployment.yaml) for file in "${files[@]}"; do [[ -f "$file" ]] || continue # Replace dots with hyphens in container names (spec.template.spec.containers[].name) python - "$file" <<'PY' import sys import re path = sys.argv[1] with open(path, 'r', encoding='utf-8') as f: content = f.read() def replace_dots(match): prefix = match.group(1) name = match.group(2) if '.' in name: return prefix + name.replace('.', '-') return match.group(0) # Match 'name: some.container.name' with at least 4 spaces of indentation # This helps avoid metadata.name which usually has 2 spaces. new_content = re.sub(r'(\s{4,}name:\s+)([^\n]+)', replace_dots, content) if new_content != content: with open(path, 'w', encoding='utf-8') as f: f.write(new_content) PY done } helm_render_values() { local renderer="$PROJECT_ROOT/supabase/helm/render_supabase.py" [[ -f "$renderer" ]] || die "Renderer not found: $renderer" local cfg_arg=() if [[ -n "$PROLE_CFG_PATH" ]]; then cfg_arg=("-c" "$PROLE_CFG_PATH") fi log "Rendering Supabase Helm values/manifests..." python3 "$renderer" "${cfg_arg[@]}" \ --output-dir "$PROJECT_ROOT/supabase/helm/generated" \ --manifests-dir "$PROJECT_ROOT/supabase/k8s" } run_helm() { if [[ "$MODE" == "local" ]]; then return 1 fi ensure_helm require_cmd kubectl kubectl cluster-info >/dev/null 2>&1 || return 1 if [[ "$FORCE" == "true" ]]; then # Helm stores release metadata as Secrets in the namespace; it will fail if # the namespace is stuck in Terminating. force_reset_supabase_namespace else ensure_supabase_namespace fi helm_render_values setup_prole_db_for_supabase local summary="$PROJECT_ROOT/supabase/helm/generated/manifest-summary.json" local values="$PROJECT_ROOT/supabase/helm/generated/values.generated.json" local ns="supabase" if [[ -f "$summary" ]]; then ns=$(python3 - "$summary" <<'PY' import json, sys try: path = sys.argv[1] data = json.load(open(path)) print(data.get("supabase_namespace","supabase")) except Exception: print("supabase") PY ) fi if [[ "$HELM_TEMPLATE_ONLY" == "true" ]]; then log "Helm template only; manifests ready in supabase/k8s" return 0 fi log "Installing Supabase via Helm into namespace '$ns'..." if helm upgrade --install "$HELM_RELEASE" "$PROJECT_ROOT/supabase/helm/prole-supabase" \ -n "$ns" --create-namespace -f "$values"; then register_supabase_ports log "Deployment complete via Helm." return 0 else warn "Helm install failed; falling back to legacy kompose flow." return 1 fi } ensure_supabase_namespace() { if kubectl get namespace supabase >/dev/null 2>&1; then local phase phase=$(kubectl get namespace supabase -o jsonpath='{.status.phase}' 2>/dev/null || true) if [[ "${phase:-}" == "Terminating" ]]; then warn "Namespace 'supabase' is Terminating; resetting it" force_reset_supabase_namespace fi return 0 fi log "Creating 'supabase' namespace..." kubectl create namespace supabase } force_reset_supabase_namespace() { if ! kubectl get namespace supabase >/dev/null 2>&1; then ensure_supabase_namespace return fi log "Resetting 'supabase' namespace..." # Delete all workloads first to avoid finalizer stalls kubectl delete all --all -n supabase --timeout=60s >/dev/null 2>&1 || true # Request namespace deletion without blocking kubectl delete namespace supabase --ignore-not-found --wait=false >/dev/null 2>&1 || true if kubectl get namespace supabase >/dev/null 2>&1; then if ! kubectl wait --for=delete namespace/supabase --timeout=120s >/dev/null 2>&1; then warn "Namespace deletion stalled; clearing finalizers" python - <<'PY' | kubectl replace --raw "/api/v1/namespaces/supabase/finalize" -f - >/dev/null 2>&1 || true import json import subprocess import sys try: raw = subprocess.check_output(["kubectl", "get", "namespace", "supabase", "-o", "json"]) if not raw: sys.exit(0) data = json.loads(raw) if "spec" in data: data["spec"]["finalizers"] = [] print(json.dumps(data)) except Exception: sys.exit(0) PY kubectl wait --for=delete namespace/supabase --timeout=60s >/dev/null 2>&1 || true fi fi ensure_supabase_namespace } run_k3d() { # Helm-first path (preferred) if run_helm; then return 0 fi ensure_repo log "Prole::Supabase k3d deploy (Kubernetes)" log "Repo: $SUPABASE_DIR" require_cmd kubectl ensure_k3d ensure_k3d_cluster kubectl cluster-info >/dev/null 2>&1 || die "Kubernetes cluster not reachable" if [[ "$FORCE" == "true" ]]; then force_reset_supabase_namespace else ensure_supabase_namespace fi export SUPABASE_HOME="$SUPABASE_DIR" if [[ "$USE_DEV_HELPERS" == "true" ]]; then export SUPABASE_USE_DEV_COMPOSE=1 fi export SUPABASE_IMAGE_PLATFORM SUPABASE_IMAGE_PLATFORM="$(normalize_platform "${SUPABASE_IMAGE_PLATFORM:-}")" if [[ "$SKIP_PREFETCH" != "true" ]]; then prefetch_k3d_images else log "Skipping Supabase image prefetch (requested)." fi generate_k8s_manifests setup_prole_db_for_supabase if [[ -f "$SUPABASE_K8S_DIR/supabase-namespace.yaml" ]]; then kubectl apply -f "$SUPABASE_K8S_DIR/supabase-namespace.yaml" fi log "Applying Supabase manifests from $SUPABASE_K8S_DIR" kubectl apply -f "$SUPABASE_K8S_DIR" register_supabase_ports log "Deployment complete." log "Access Studio via Kong proxy (check ingress/service in 'supabase' namespace)." } run_prefetch_images_only() { ensure_repo case "$MODE" in k3d) require_cmd kubectl ensure_k3d ensure_k3d_cluster kubectl cluster-info >/dev/null 2>&1 || die "Kubernetes cluster not reachable" export SUPABASE_HOME="$SUPABASE_DIR" if [[ "$USE_DEV_HELPERS" == "true" ]]; then export SUPABASE_USE_DEV_COMPOSE=1 fi export SUPABASE_IMAGE_PLATFORM SUPABASE_IMAGE_PLATFORM="$(normalize_platform "${SUPABASE_IMAGE_PLATFORM:-}")" prefetch_k3d_images log "Supabase image prefetch complete." ;; *) die "--prefetch-images-only currently supports only --mode k3d" ;; esac } run_k8s() { # Helm-first path (preferred) if run_helm; then return 0 fi ensure_repo log "Prole::Supabase k8s deploy (Kubernetes - generic/containerd)" log "Repo: $SUPABASE_DIR" require_cmd kubectl kubectl cluster-info >/dev/null 2>&1 || die "Kubernetes cluster not reachable" if [[ "$FORCE" == "true" ]]; then force_reset_supabase_namespace else ensure_supabase_namespace fi export SUPABASE_HOME="$SUPABASE_DIR" if [[ "$USE_DEV_HELPERS" == "true" ]]; then export SUPABASE_USE_DEV_COMPOSE=1 fi # For generic k8s/k3s clusters (containerd), let nodes pull appropriate arch images directly # Optionally, a future enhancement could push pre-fetched images to an internal registry. generate_k8s_manifests setup_prole_db_for_supabase if [[ -f "$SUPABASE_K8S_DIR/supabase-namespace.yaml" ]]; then kubectl apply -f "$SUPABASE_K8S_DIR/supabase-namespace.yaml" fi log "Applying Supabase manifests from $SUPABASE_K8S_DIR" kubectl apply -f "$SUPABASE_K8S_DIR" register_supabase_ports log "Deployment complete." log "Access Studio via Kong proxy (check ingress/service in 'supabase' namespace)." } main() { parse_args "$@" load_prole_cfg apply_defaults if [[ -z "$MODE" ]]; then usage exit 1 fi if [[ "$PREFETCH_IMAGES_ONLY" == "true" ]]; then run_prefetch_images_only return 0 fi case "$MODE" in local) run_local ;; k3d) run_k3d ;; k8s) run_k8s ;; *) die "Unsupported mode: $MODE" ;; esac } main "$@"