mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 12:03:59 +00:00
All 9 Supabase pods Running on merlin.prole.org backed by merlin-local-iscsi-d002
(/synology/d002) iSCSI storage. db.prole.org (HTTP 401 = auth gate = reachable).
CNPG cluster knoe-db 2/2 Ready (merlin + myrddin), Percona PostgreSQL 18, Barman backup.
Key fixes in this build:
- supabase_enabled=true now honored in silent mode (actions.py override removed)
- SUPABASE_STORAGE_CLASS/PV_NODE/PRIMARY_NODE pinned to merlin in prole.cfg
- _write_cfg preserves SUPABASE_* vars from existing cfg (no pipeline write-back clobber)
- render_supabase.py writes persistence.*.storageClassName (correct Helm chart path)
- deploy.sh: synology guard prevents non-iSCSI storage; --set persistence.* all modes;
stale values.generated.json deleted before every render; live DB_PASSWORD from secret
- init_monitoring.sh: pv_prom/pv_am/pv_graf derived inline before Released-PV loop
- k8s/prole/knoe-db.yaml: instances=2 (pi excluded), maxSyncReplicas=0
- Static PVs merlin-supabase-{functions,imgproxy,snippets,storage} created on merlin
- storage.migrations DROP fixes idempotent reinstall conflict from prior deploy
Co-authored-by: Junie <junie@jetbrains.com>
1847 lines
53 KiB
Bash
Executable File
1847 lines
53 KiB
Bash
Executable File
#!/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 <local|k3d|k8s> 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> 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}"
|
|
KNOE_DB_SERVICE="${KNOE_DB_SERVICE:-knoe-db-rw}"
|
|
KNOE_DB_NAMESPACE="${KNOE_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"
|
|
}
|
|
|
|
duration_to_seconds() {
|
|
# Accepts: 900, 900s, 15m, 1h
|
|
local raw="${1:-}"
|
|
raw="${raw//[[:space:]]/}"
|
|
if [[ -z "$raw" ]]; then
|
|
echo 900
|
|
return 0
|
|
fi
|
|
if [[ "$raw" =~ ^[0-9]+$ ]]; then
|
|
echo "$raw"
|
|
return 0
|
|
fi
|
|
if [[ "$raw" =~ ^([0-9]+)s$ ]]; then
|
|
echo "${BASH_REMATCH[1]}"
|
|
return 0
|
|
fi
|
|
if [[ "$raw" =~ ^([0-9]+)m$ ]]; then
|
|
echo $((BASH_REMATCH[1] * 60))
|
|
return 0
|
|
fi
|
|
if [[ "$raw" =~ ^([0-9]+)h$ ]]; then
|
|
echo $((BASH_REMATCH[1] * 3600))
|
|
return 0
|
|
fi
|
|
echo 900
|
|
}
|
|
|
|
supabase_rollout_status() {
|
|
local ns="$1"
|
|
python3 - "$ns" <<'PY'
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
|
|
ns = sys.argv[1]
|
|
|
|
def kubectl_json(*args):
|
|
out = subprocess.check_output(["kubectl", *args], text=True)
|
|
return json.loads(out)
|
|
|
|
pvc = kubectl_json("get", "pvc", "-n", ns, "-o", "json")
|
|
pods = kubectl_json("get", "pods", "-n", ns, "-o", "json")
|
|
|
|
pvc_pending = []
|
|
for item in pvc.get("items", []) or []:
|
|
name = (item.get("metadata") or {}).get("name")
|
|
phase = ((item.get("status") or {}).get("phase") or "")
|
|
if phase != "Bound":
|
|
pvc_pending.append(name or "?")
|
|
|
|
not_ready = []
|
|
terminating = 0
|
|
for item in pods.get("items", []) or []:
|
|
md = item.get("metadata") or {}
|
|
name = md.get("name") or "?"
|
|
if md.get("deletionTimestamp"):
|
|
terminating += 1
|
|
continue
|
|
status = item.get("status") or {}
|
|
phase = status.get("phase") or ""
|
|
if phase in ("Succeeded",):
|
|
continue
|
|
# Consider Failed / Pending / Running-not-ready as not ready
|
|
ready = False
|
|
for cond in status.get("conditions", []) or []:
|
|
if cond.get("type") == "Ready" and cond.get("status") == "True":
|
|
ready = True
|
|
break
|
|
if not ready:
|
|
not_ready.append(name)
|
|
|
|
print(json.dumps({
|
|
"pvcPending": pvc_pending,
|
|
"podsNotReady": not_ready,
|
|
"podsTerminating": terminating,
|
|
}))
|
|
PY
|
|
}
|
|
|
|
wait_for_supabase_ready() {
|
|
local ns="$1"
|
|
local timeout_s="$2"
|
|
local start
|
|
start=$(date +%s)
|
|
|
|
local poll_s
|
|
poll_s="${SUPABASE_DEPLOY_POLL_SECONDS:-10}"
|
|
if [[ -z "$poll_s" ]]; then
|
|
poll_s=10
|
|
fi
|
|
|
|
log "Waiting for Supabase readiness in namespace '${ns}' (timeout=${timeout_s}s)..."
|
|
|
|
while true; do
|
|
local now elapsed
|
|
now=$(date +%s)
|
|
elapsed=$((now - start))
|
|
|
|
local status_json
|
|
if ! status_json=$(supabase_rollout_status "$ns" 2>/dev/null); then
|
|
warn "Could not query Supabase rollout status (ns=${ns}); retrying..."
|
|
if (( elapsed >= timeout_s )); then
|
|
return 1
|
|
fi
|
|
sleep "$poll_s"
|
|
continue
|
|
fi
|
|
|
|
local pending_pvcs not_ready_pods terminating_pods
|
|
pending_pvcs=$(python3 - <<PY
|
|
import json,sys
|
|
data=json.loads('''$status_json''')
|
|
print(len(data.get('pvcPending') or []))
|
|
PY
|
|
)
|
|
not_ready_pods=$(python3 - <<PY
|
|
import json,sys
|
|
data=json.loads('''$status_json''')
|
|
print(len(data.get('podsNotReady') or []))
|
|
PY
|
|
)
|
|
terminating_pods=$(python3 - <<PY
|
|
import json,sys
|
|
data=json.loads('''$status_json''')
|
|
print(int(data.get('podsTerminating') or 0))
|
|
PY
|
|
)
|
|
|
|
if (( pending_pvcs == 0 && not_ready_pods == 0 && terminating_pods == 0 )); then
|
|
log "Supabase is Ready (all PVCs bound; all pods ready)."
|
|
return 0
|
|
fi
|
|
|
|
if (( elapsed >= timeout_s )); then
|
|
warn "Supabase readiness timed out after ${elapsed}s (pendingPVCs=${pending_pvcs}, notReadyPods=${not_ready_pods}, terminatingPods=${terminating_pods})"
|
|
return 1
|
|
fi
|
|
|
|
if (( elapsed % 30 == 0 )); then
|
|
log "Still waiting: elapsed=${elapsed}s pendingPVCs=${pending_pvcs} notReadyPods=${not_ready_pods} terminatingPods=${terminating_pods}"
|
|
fi
|
|
|
|
sleep "$poll_s"
|
|
done
|
|
}
|
|
|
|
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_knoe_db_namespace() {
|
|
if [[ -n "${KNOE_DB_NAMESPACE:-}" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
if [[ -n "${NAMESPACE:-}" ]]; then
|
|
KNOE_DB_NAMESPACE="$NAMESPACE"
|
|
return 0
|
|
fi
|
|
|
|
if kubectl get namespace prole >/dev/null 2>&1; then
|
|
KNOE_DB_NAMESPACE="prole"
|
|
else
|
|
KNOE_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" <<EOF
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: supabase-postgres
|
|
namespace: supabase
|
|
labels:
|
|
app: supabase-postgres
|
|
spec:
|
|
type: ExternalName
|
|
externalName: ${KNOE_DB_SERVICE}.${KNOE_DB_NAMESPACE}.svc.cluster.local
|
|
ports:
|
|
- name: postgres
|
|
port: ${SUPABASE_POSTGRES_PORT}
|
|
targetPort: 5432
|
|
EOF
|
|
}
|
|
|
|
write_db_alias_service() {
|
|
local file="$SUPABASE_K8S_DIR/db-service.yaml"
|
|
|
|
cat > "$file" <<EOF
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: db
|
|
namespace: supabase
|
|
labels:
|
|
app: supabase-db
|
|
spec:
|
|
type: ExternalName
|
|
externalName: ${KNOE_DB_SERVICE}.${KNOE_DB_NAMESPACE}.svc.cluster.local
|
|
ports:
|
|
- name: postgres
|
|
port: 5432
|
|
targetPort: 5432
|
|
EOF
|
|
}
|
|
|
|
|
|
fix_k8s_manifests() {
|
|
log "Post-processing Kubernetes manifests for k8s compatibility..."
|
|
|
|
# 1. Fix db data volume: configMap -> 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 knoe-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 knoe-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 knoe-db-superuser secret
|
|
resolve_knoe_db_namespace
|
|
local ns="${KNOE_DB_NAMESPACE:-${NAMESPACE:-default}}"
|
|
if kubectl get secret knoe-db-superuser -n "$ns" >/dev/null 2>&1; then
|
|
pg_password=$(kubectl get secret knoe-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 knoe-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_knoe_db_for_supabase() {
|
|
log "Ensuring Supabase roles, schemas and databases exist in knoe-db..."
|
|
|
|
resolve_knoe_db_namespace
|
|
local ns="${KNOE_DB_NAMESPACE:-${NAMESPACE:-default}}"
|
|
|
|
# Find the primary CNPG pod
|
|
local primary
|
|
primary=$(kubectl -n "$ns" get pods \
|
|
-l "cnpg.io/cluster=knoe-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=knoe-db" \
|
|
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
|
|
fi
|
|
if [[ -z "$primary" ]]; then
|
|
warn "No knoe-db pod found in namespace '$ns'; skipping Supabase DB setup."
|
|
return 0
|
|
fi
|
|
|
|
local pg_password=""
|
|
if kubectl get secret knoe-db-superuser -n "$ns" >/dev/null 2>&1; then
|
|
pg_password=$(kubectl get secret knoe-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
|
|
-- Drop stale storage migrations table so storage service can re-run cleanly (idempotent reinstall)
|
|
DROP TABLE IF EXISTS storage.migrations CASCADE;
|
|
|
|
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 \$\$;
|
|
|
|
-- Always sync service role passwords to current postgres password (idempotent)
|
|
ALTER ROLE authenticator PASSWORD '${pg_password}';
|
|
ALTER ROLE pgbouncer PASSWORD '${pg_password}';
|
|
ALTER ROLE supabase_admin PASSWORD '${pg_password}';
|
|
ALTER ROLE supabase_auth_admin PASSWORD '${pg_password}';
|
|
ALTER ROLE supabase_storage_admin PASSWORD '${pg_password}';
|
|
ALTER ROLE supabase_functions_admin PASSWORD '${pg_password}';
|
|
|
|
-- 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_knoe_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
|
|
|
|
# Resolve the live postgres password from the CNPG secret so the renderer
|
|
# always uses the actual cluster password rather than the (potentially stale)
|
|
# value stored in prole.cfg.
|
|
local _live_pg_pw
|
|
_live_pg_pw=$(kubectl get secret knoe-db-superuser \
|
|
-n "${DATABASE_NAMESPACE:-knoe-db}" \
|
|
-o jsonpath='{.data.password}' 2>/dev/null | base64 --decode 2>/dev/null || true)
|
|
if [[ -n "$_live_pg_pw" ]]; then
|
|
export DB_PASSWORD="$_live_pg_pw"
|
|
fi
|
|
|
|
# Always regenerate — never reuse stale cached values from a previous deploy attempt
|
|
rm -f "$PROJECT_ROOT/supabase/helm/generated/values.generated.json"
|
|
|
|
log "Rendering Supabase Helm values/manifests..."
|
|
python3 "$renderer" "${cfg_arg[@]}" \
|
|
--output-dir "$PROJECT_ROOT/supabase/helm/generated" \
|
|
--manifests-dir "$PROJECT_ROOT/supabase/k8s"
|
|
}
|
|
|
|
ensure_k8s_supabase_static_pvs() {
|
|
local storage_class="$1"
|
|
if [[ "${MODE:-}" != "k8s" ]]; then
|
|
return 0
|
|
fi
|
|
if [[ -z "${storage_class:-}" || "$storage_class" != "synology-iscsi" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
require_cmd kubectl
|
|
|
|
local node base parent base_name
|
|
node="${SUPABASE_PV_NODE:-merlin.prole.org}"
|
|
base="${SUPABASE_PV_BASE_DIR:-/synology/d002/supabase}"
|
|
base="${base%/}"
|
|
parent="$(dirname "$base")"
|
|
base_name="$(basename "$base")"
|
|
|
|
log "Ensuring static Supabase PV directories exist on node '${node}' (base=${base})..."
|
|
|
|
local job_ns job_name timeout image
|
|
job_ns="${SUPABASE_DIRPREP_NAMESPACE:-kube-system}"
|
|
job_name="${SUPABASE_DIRPREP_JOB_NAME:-prole-supabase-dirprep}"
|
|
timeout="${SUPABASE_DIRPREP_TIMEOUT:-120s}"
|
|
image="${SUPABASE_DIRPREP_IMAGE:-alpine:3.20}"
|
|
|
|
kubectl -n "$job_ns" delete job "$job_name" --ignore-not-found >/dev/null 2>&1 || true
|
|
|
|
cat <<EOF | kubectl apply -f - >/dev/null
|
|
apiVersion: batch/v1
|
|
kind: Job
|
|
metadata:
|
|
name: ${job_name}
|
|
namespace: ${job_ns}
|
|
spec:
|
|
backoffLimit: 0
|
|
template:
|
|
spec:
|
|
restartPolicy: Never
|
|
nodeName: ${node}
|
|
containers:
|
|
- name: dirprep
|
|
image: ${image}
|
|
securityContext:
|
|
runAsUser: 0
|
|
runAsGroup: 0
|
|
command: ["/bin/sh", "-ec"]
|
|
args:
|
|
- |
|
|
BASE_DIR="/host-parent/${base_name}"
|
|
echo "[dirprep] node=${node} base=${base}"
|
|
mkdir -p "\${BASE_DIR}"
|
|
mkdir -p "\${BASE_DIR}/db" "\${BASE_DIR}/deno" "\${BASE_DIR}/functions" "\${BASE_DIR}/imgproxy" "\${BASE_DIR}/snippets" "\${BASE_DIR}/storage"
|
|
chmod 0777 "\${BASE_DIR}" 2>/dev/null || true
|
|
chmod 0777 "\${BASE_DIR}/db" "\${BASE_DIR}/deno" "\${BASE_DIR}/functions" "\${BASE_DIR}/imgproxy" "\${BASE_DIR}/snippets" "\${BASE_DIR}/storage" 2>/dev/null || true
|
|
echo "[dirprep][OK] Prepared ${base}/{db,deno,functions,imgproxy,snippets,storage}"
|
|
volumeMounts:
|
|
- name: host-parent
|
|
mountPath: /host-parent
|
|
volumes:
|
|
- name: host-parent
|
|
hostPath:
|
|
path: ${parent}
|
|
type: Directory
|
|
EOF
|
|
|
|
if ! kubectl -n "$job_ns" wait --for=condition=complete "job/${job_name}" --timeout="$timeout" >/dev/null 2>&1; then
|
|
warn "Supabase directory preparation job did not complete successfully (job=${job_name} ns=${job_ns})."
|
|
set +e
|
|
kubectl -n "$job_ns" logs "job/${job_name}" 2>/dev/null || true
|
|
kubectl -n "$job_ns" describe "job/${job_name}" 2>/dev/null || true
|
|
kubectl -n "$job_ns" get pod -l job-name="${job_name}" -o wide 2>/dev/null || true
|
|
set -e
|
|
return 1
|
|
fi
|
|
|
|
set +e
|
|
kubectl -n "$job_ns" logs "job/${job_name}" 2>/dev/null || true
|
|
kubectl -n "$job_ns" delete job "$job_name" --ignore-not-found >/dev/null 2>&1 || true
|
|
set -e
|
|
|
|
# Static local PVs with Retain reclaimPolicy can get stuck in Released after a namespace reset.
|
|
# Delete Released PV objects so they can be recreated and rebound to the new PVCs.
|
|
local pvs=(
|
|
synology-supabase-db
|
|
synology-supabase-deno
|
|
synology-supabase-functions
|
|
synology-supabase-imgproxy
|
|
synology-supabase-snippets
|
|
synology-supabase-storage
|
|
)
|
|
local pv
|
|
for pv in "${pvs[@]}"; do
|
|
local phase
|
|
phase=$(kubectl get pv "$pv" -o jsonpath='{.status.phase}' 2>/dev/null || true)
|
|
if [[ "${phase:-}" == "Released" ]]; then
|
|
warn "PV '$pv' is in Released state; deleting PV object to allow rebind"
|
|
kubectl delete pv "$pv" --ignore-not-found >/dev/null 2>&1 || true
|
|
fi
|
|
done
|
|
|
|
log "Applying static Supabase PersistentVolumes (storageClass=${storage_class}, node=${node})..."
|
|
cat <<EOF | kubectl apply -f -
|
|
apiVersion: v1
|
|
kind: PersistentVolume
|
|
metadata:
|
|
name: synology-supabase-db
|
|
labels:
|
|
synology.storage/pvc: supabase-db
|
|
spec:
|
|
capacity:
|
|
storage: 5Gi
|
|
volumeMode: Filesystem
|
|
accessModes:
|
|
- ReadWriteOnce
|
|
storageClassName: ${storage_class}
|
|
persistentVolumeReclaimPolicy: Retain
|
|
local:
|
|
path: ${base}/db
|
|
nodeAffinity:
|
|
required:
|
|
nodeSelectorTerms:
|
|
- matchExpressions:
|
|
- key: kubernetes.io/hostname
|
|
operator: In
|
|
values:
|
|
- ${node}
|
|
---
|
|
apiVersion: v1
|
|
kind: PersistentVolume
|
|
metadata:
|
|
name: synology-supabase-deno
|
|
labels:
|
|
synology.storage/pvc: supabase-deno
|
|
spec:
|
|
capacity:
|
|
storage: 1Gi
|
|
volumeMode: Filesystem
|
|
accessModes:
|
|
- ReadWriteOnce
|
|
storageClassName: ${storage_class}
|
|
persistentVolumeReclaimPolicy: Retain
|
|
local:
|
|
path: ${base}/deno
|
|
nodeAffinity:
|
|
required:
|
|
nodeSelectorTerms:
|
|
- matchExpressions:
|
|
- key: kubernetes.io/hostname
|
|
operator: In
|
|
values:
|
|
- ${node}
|
|
---
|
|
apiVersion: v1
|
|
kind: PersistentVolume
|
|
metadata:
|
|
name: synology-supabase-functions
|
|
labels:
|
|
synology.storage/pvc: supabase-functions
|
|
spec:
|
|
capacity:
|
|
storage: 1Gi
|
|
volumeMode: Filesystem
|
|
accessModes:
|
|
- ReadWriteOnce
|
|
storageClassName: ${storage_class}
|
|
persistentVolumeReclaimPolicy: Retain
|
|
local:
|
|
path: ${base}/functions
|
|
nodeAffinity:
|
|
required:
|
|
nodeSelectorTerms:
|
|
- matchExpressions:
|
|
- key: kubernetes.io/hostname
|
|
operator: In
|
|
values:
|
|
- ${node}
|
|
---
|
|
apiVersion: v1
|
|
kind: PersistentVolume
|
|
metadata:
|
|
name: synology-supabase-imgproxy
|
|
labels:
|
|
synology.storage/pvc: supabase-imgproxy
|
|
spec:
|
|
capacity:
|
|
storage: 1Gi
|
|
volumeMode: Filesystem
|
|
accessModes:
|
|
- ReadWriteOnce
|
|
storageClassName: ${storage_class}
|
|
persistentVolumeReclaimPolicy: Retain
|
|
local:
|
|
path: ${base}/imgproxy
|
|
nodeAffinity:
|
|
required:
|
|
nodeSelectorTerms:
|
|
- matchExpressions:
|
|
- key: kubernetes.io/hostname
|
|
operator: In
|
|
values:
|
|
- ${node}
|
|
---
|
|
apiVersion: v1
|
|
kind: PersistentVolume
|
|
metadata:
|
|
name: synology-supabase-snippets
|
|
labels:
|
|
synology.storage/pvc: supabase-snippets
|
|
spec:
|
|
capacity:
|
|
storage: 1Gi
|
|
volumeMode: Filesystem
|
|
accessModes:
|
|
- ReadWriteOnce
|
|
storageClassName: ${storage_class}
|
|
persistentVolumeReclaimPolicy: Retain
|
|
local:
|
|
path: ${base}/snippets
|
|
nodeAffinity:
|
|
required:
|
|
nodeSelectorTerms:
|
|
- matchExpressions:
|
|
- key: kubernetes.io/hostname
|
|
operator: In
|
|
values:
|
|
- ${node}
|
|
---
|
|
apiVersion: v1
|
|
kind: PersistentVolume
|
|
metadata:
|
|
name: synology-supabase-storage
|
|
labels:
|
|
synology.storage/pvc: supabase-storage
|
|
spec:
|
|
capacity:
|
|
storage: 1Gi
|
|
volumeMode: Filesystem
|
|
accessModes:
|
|
- ReadWriteOnce
|
|
storageClassName: ${storage_class}
|
|
persistentVolumeReclaimPolicy: Retain
|
|
local:
|
|
path: ${base}/storage
|
|
nodeAffinity:
|
|
required:
|
|
nodeSelectorTerms:
|
|
- matchExpressions:
|
|
- key: kubernetes.io/hostname
|
|
operator: In
|
|
values:
|
|
- ${node}
|
|
EOF
|
|
}
|
|
|
|
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_knoe_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
|
|
|
|
local storage_class
|
|
storage_class="${SUPABASE_STORAGE_CLASS:-}"
|
|
# Fall back to cfg file if env var not exported into this shell
|
|
if [[ -z "$storage_class" && -n "${PROLE_CFG_PATH:-}" ]]; then
|
|
storage_class=$(python3 -c "
|
|
import configparser, sys
|
|
c=configparser.ConfigParser(); c.read('${PROLE_CFG_PATH}')
|
|
print(c.get('Global','SUPABASE_STORAGE_CLASS',fallback=''))" 2>/dev/null || true)
|
|
fi
|
|
if [[ -z "$storage_class" && "$MODE" == "k8s" ]]; then
|
|
storage_class="synology-iscsi"
|
|
fi
|
|
# Guard: refuse non-synology storage classes to prevent SD-card crash on pi
|
|
if [[ -n "$storage_class" ]] && ! [[ "$storage_class" =~ ^(synology|merlin-local-iscsi|myrddin-local-iscsi) ]]; then
|
|
die "SUPABASE_STORAGE_CLASS '${storage_class}' is not a synology/iSCSI mount. Refusing Supabase deploy to prevent node crash."
|
|
fi
|
|
|
|
ensure_k8s_supabase_static_pvs "$storage_class"
|
|
# k3s: apply static PV manifest (idempotent) so PVCs can bind on merlin
|
|
if [[ "${MODE:-}" == "k3s" && -f "$PROJECT_ROOT/k8s/prole/supabase-pvs.yaml" ]]; then
|
|
log "Applying Supabase static PVs from k8s/prole/supabase-pvs.yaml..."
|
|
kubectl apply -f "$PROJECT_ROOT/k8s/prole/supabase-pvs.yaml" 2>&1 || true
|
|
fi
|
|
|
|
local helm_set_args=()
|
|
if [[ -n "$storage_class" ]]; then
|
|
# Apply to every PVC the chart can create — works for all modes
|
|
helm_set_args+=(
|
|
--set "persistence.db.storageClassName=${storage_class}"
|
|
--set "persistence.deno.storageClassName=${storage_class}"
|
|
--set "persistence.functions.storageClassName=${storage_class}"
|
|
--set "persistence.imgproxy.storageClassName=${storage_class}"
|
|
--set "persistence.minio.storageClassName=${storage_class}"
|
|
--set "persistence.snippets.storageClassName=${storage_class}"
|
|
--set "persistence.storage.storageClassName=${storage_class}"
|
|
)
|
|
fi
|
|
|
|
local timeout_raw timeout_s
|
|
timeout_raw="${SUPABASE_DEPLOY_TIMEOUT:-15m}"
|
|
timeout_s=$(duration_to_seconds "$timeout_raw")
|
|
|
|
local max_attempts
|
|
max_attempts="${SUPABASE_DEPLOY_MAX_ATTEMPTS:-2}"
|
|
if [[ -z "$max_attempts" ]]; then
|
|
max_attempts=2
|
|
fi
|
|
|
|
local attempt
|
|
attempt=1
|
|
while (( attempt <= max_attempts )); do
|
|
if (( attempt > 1 )); then
|
|
warn "Retrying Supabase Helm deploy after timeout (attempt ${attempt}/${max_attempts})"
|
|
force_reset_supabase_namespace
|
|
helm_render_values
|
|
setup_knoe_db_for_supabase
|
|
ensure_k8s_supabase_static_pvs "$storage_class"
|
|
fi
|
|
|
|
log "Installing Supabase via Helm into namespace '$ns' (attempt ${attempt}/${max_attempts})..."
|
|
if ! helm upgrade --install "$HELM_RELEASE" "$PROJECT_ROOT/supabase/helm/prole-supabase" \
|
|
-n "$ns" --create-namespace -f "$values" "${helm_set_args[@]}"; then
|
|
warn "Helm install failed; falling back to legacy kompose flow."
|
|
return 1
|
|
fi
|
|
|
|
local wait_enabled
|
|
wait_enabled="${SUPABASE_DEPLOY_WAIT:-true}"
|
|
if [[ "$wait_enabled" == "true" ]]; then
|
|
if wait_for_supabase_ready "$ns" "$timeout_s"; then
|
|
register_supabase_ports
|
|
log "Deployment complete via Helm."
|
|
return 0
|
|
fi
|
|
|
|
if (( attempt >= max_attempts )); then
|
|
warn "Supabase Helm deploy did not become Ready within timeout after ${attempt} attempt(s)."
|
|
# Return a distinct code so k8s mode doesn't fall back to legacy manifests.
|
|
return 2
|
|
fi
|
|
else
|
|
register_supabase_ports
|
|
log "Deployment complete via Helm (wait disabled)."
|
|
return 0
|
|
fi
|
|
|
|
attempt=$((attempt + 1))
|
|
done
|
|
|
|
return 2
|
|
}
|
|
|
|
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
|
|
|
|
# PVCs can keep pods around and block namespace deletion; remove them explicitly.
|
|
kubectl delete pvc --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)
|
|
run_helm
|
|
local helm_rc=$?
|
|
if [[ $helm_rc -eq 0 ]]; then
|
|
return 0
|
|
fi
|
|
if [[ $helm_rc -eq 2 ]]; then
|
|
die "Supabase Helm deploy timed out waiting for readiness; aborting (no legacy fallback)."
|
|
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_knoe_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)
|
|
run_helm
|
|
local helm_rc=$?
|
|
if [[ $helm_rc -eq 0 ]]; then
|
|
return 0
|
|
fi
|
|
if [[ $helm_rc -eq 2 ]]; then
|
|
die "Supabase Helm deploy timed out waiting for readiness; aborting (no legacy fallback)."
|
|
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_knoe_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 "$@"
|