Checkpoint: Refactor installer UI and update Supabase deployment strategy

- Refactored install.py and installer package for improved UI and navigation.

- Replaced Supabase k8s manifests with a dedicated deployment script and port-wiring logic.

- Added new deployment pipeline and finalization scripts in etc/.

- Updated initialization scripts for Kerberos, authority, and port forwards.

- Updated port mappings and tests.
This commit is contained in:
chrisfu 2026-02-03 14:55:05 -08:00
parent 12d00c468a
commit edbc6c5385
18 changed files with 2129 additions and 1333 deletions

View File

@ -1,9 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<portMappings>
<mapping id="dashboard" namespace="kubernetes-dashboard" target="svc/kubernetes-dashboard-kong-proxy" address="127.0.0.1" hostPort="8443" servicePort="443" protocol="TCP" description="Kubernetes Dashboard (https://127.0.0.1:8443)"/>
<mapping id="prometheus" namespace="${NAMESPACE}" target="svc/prometheus-community-kube-prometheus" address="127.0.0.1" hostPort="9090" servicePort="9090" protocol="TCP" description="Prometheus UI (http://127.0.0.1:9090)"/>
<mapping id="prometheus" namespace="default" target="svc/prometheus-community-kube-prometheus" address="127.0.0.1" hostPort="9090" servicePort="9090" protocol="TCP" description="Prometheus UI (http://127.0.0.1:9090)"/>
<mapping id="grafana" namespace="${NAMESPACE}" target="svc/grafana-prole" address="0.0.0.0" hostPort="3000" servicePort="80" protocol="TCP" description="Grafana UI (http://127.0.0.1:3000)"/>
<mapping id="postgres" namespace="${NAMESPACE}" target="svc/prole-db-rw" address="0.0.0.0" hostPort="5432" servicePort="5432" protocol="TCP" description="PostgreSQL (primary) (127.0.0.1:5432)"/>
<mapping id="prole-db" namespace="${NAMESPACE}" target="svc/prole-db-rw" address="0.0.0.0" hostPort="15432" servicePort="5432" protocol="TCP" description="Prole DB (alternate port when Supabase is enabled)"/>
<mapping id="supabase-db" namespace="supabase" target="svc/db" address="0.0.0.0" hostPort="5432" servicePort="5432" protocol="TCP" description="Supabase PostgreSQL (127.0.0.1:5432)"/>
<mapping id="openbao" namespace="${NAMESPACE}" target="svc/openbao" address="127.0.0.1" hostPort="8200" servicePort="8200" protocol="TCP" description="OpenBao UI (http://127.0.0.1:8200)"/>
</portMappings>

62
etc/deploy_pipeline.sh Executable file
View File

@ -0,0 +1,62 @@
#!/usr/bin/env bash
set -euo pipefail
# etc/deploy_pipeline.sh
# Collects artifacts and prepares terraform directory for GCP deployment
usage() {
echo "Usage: $0 --mode <gcp>"
echo "Options:"
echo " --mode gcp Prepare and publish the terraform pipeline to GCP"
exit 1
}
if [[ $# -eq 0 ]]; then
usage
fi
MODE=""
while [[ $# -gt 0 ]]; do
case "$1" in
--mode)
MODE="$2"
shift 2
;;
*)
usage
;;
esac
done
if [[ "$MODE" != "gcp" ]]; then
echo "Error: Only 'gcp' mode is supported currently."
usage
fi
# Mode GCP Implementation
echo "Preparing GCP deployment pipeline..."
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
STAGING_DIR="${STAGING_DIR:-$PROJECT_ROOT/data/staging}"
TF_DIR="$PROJECT_ROOT/deploy/gcp/terraform"
if [[ ! -d "$STAGING_DIR" ]]; then
echo "Staging directory not found: $STAGING_DIR"
echo "Please ensure artifacts are staged before running this script."
exit 1
fi
echo "Collecting artifacts from $STAGING_DIR..."
# Logic to lift and shift kubernetes deployment into terraform directory
# For now, we'll just simulate this by copying relevant files or updating tfvars
# Example: copy staged manifests to a specific location in TF directory if needed
# mkdir -p "$TF_DIR/manifests"
# cp "$STAGING_DIR"/*.yaml "$TF_DIR/manifests/"
echo "Preparing terraform directory at $TF_DIR..."
# cd "$TF_DIR"
# terraform init
echo "GCP deployment pipeline prepared successfully."
echo "You can now run terraform apply in $TF_DIR"

95
etc/final_deployment.sh Executable file
View File

@ -0,0 +1,95 @@
#!/bin/bash
# usage: etc/final_deployment.sh [-e|--docker-export]
#
# This script handles post-installation deployment tasks for Prole.
# 1st use case: -e|--docker-export
# Runs docker image export for each image required to deploy Prole.
# Artifacts are written to DOCKER_IMPORT_DIR.
usage() {
echo "Usage: $0 [options]"
echo ""
echo "Options:"
echo " -e, --docker-export Export required docker images to DOCKER_IMPORT_DIR"
echo ""
}
if [[ $# -eq 0 ]]; then
usage
exit 1
fi
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export PROJECT_ROOT
# Load environment if env.sh exists
if [[ -f "${PROJECT_ROOT}/env.sh" ]]; then
source "${PROJECT_ROOT}/env.sh"
fi
DOCKER_IMPORT_DIR="${DOCKER_IMPORT_DIR:-${PROJECT_ROOT}/data/docker-import}"
export_docker_images() {
echo "Exporting Docker images to: ${DOCKER_IMPORT_DIR}"
mkdir -p "${DOCKER_IMPORT_DIR}"
# Collect images from K8s manifests
# Robust extraction of image names from YAML files
find "${PROJECT_ROOT}/k8s/prole" "${PROJECT_ROOT}/k8s/openbao" -name "*.yaml" -exec grep -h "image:" {} + | awk -F'image:' '{print $2}' | awk '{print $1}' | sed "s/['\"]//g" > /tmp/prole_images.txt
# Also check Supabase compose if it exists
if [[ -f "${PROJECT_ROOT}/supabase/docker/docker-compose.yml" ]]; then
grep -h "image:" "${PROJECT_ROOT}/supabase/docker/docker-compose.yml" | awk -F'image:' '{print $2}' | awk '{print $1}' | sed "s/['\"]//g" >> /tmp/prole_images.txt
fi
# Add Kerberos proxy image if defined
KRB_IMG="${KRB5_AD_PROXY_IMAGE:-alpine/socat}"
if [[ -n "$KRB_IMG" ]]; then
echo "$KRB_IMG" >> /tmp/prole_images.txt
fi
# Add Kerberos test image (used by init_kerberos_test.sh)
KRB_TEST_IMG="${KRB5_TEST_IMAGE:-${PROLE_KRB_TEST_IMAGE:-ubuntu:24.04}}"
if [[ -n "$KRB_TEST_IMG" ]]; then
echo "$KRB_TEST_IMG" >> /tmp/prole_images.txt
fi
# Unique images
sort -u /tmp/prole_images.txt > /tmp/prole_images_unique.txt
while read -r image; do
if [[ -z "$image" ]]; then continue; fi
# Ensure we have the image locally
echo "Checking image: $image"
if ! docker image inspect "$image" >/dev/null 2>&1; then
echo "Pulling $image..."
docker pull "$image"
fi
safe_name=$(echo "$image" | sed 's/\//_/g' | sed 's/:/_/g')
tar_path="${DOCKER_IMPORT_DIR}/${safe_name}.tar"
echo "Exporting $image to $tar_path..."
docker save "$image" -o "$tar_path"
if [[ $? -eq 0 ]]; then
echo "[OK] Exported $image"
else
echo "[ERROR] Failed to export $image"
fi
done < /tmp/prole_images_unique.txt
rm -f /tmp/prole_images.txt /tmp/prole_images_unique.txt
echo "Docker export complete."
}
case "$1" in
-e|--docker-export)
export_docker_images
;;
*)
usage
exit 1
;;
esac

View File

@ -108,12 +108,11 @@ deploy_dog() {
ansible_ssh_pub=$(cat "$HOME/.ssh/id_ed25519_ansible.pub")
fi
# Properly indent SSH keys for the YAML manifest
# Use 10 spaces to be safe inside the 'args' block's literal scalar
local ssh_pub_indented
ssh_pub_indented=$(printf '%s' "$ssh_pub" | sed 's/^/ /')
local ansible_ssh_pub_indented
ansible_ssh_pub_indented=$(printf '%s' "$ansible_ssh_pub" | sed 's/^/ /')
# Encode SSH keys to avoid YAML parsing issues from multi-line or PEM-style keys
local ssh_pub_b64
ssh_pub_b64=$(printf '%s' "$ssh_pub" | tr -d '\r' | base64 | tr -d '\n')
local ansible_ssh_pub_b64
ansible_ssh_pub_b64=$(printf '%s' "$ansible_ssh_pub" | tr -d '\r' | base64 | tr -d '\n')
# Use a separate variable for the YAML to help with debugging and clarity
local manifest
@ -146,7 +145,7 @@ spec:
# Setup root SSH
mkdir -p /root/.ssh
printf '%s\n' "$ssh_pub" > /root/.ssh/authorized_keys
printf '%s' "$ssh_pub_b64" | base64 -d > /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
# Setup ansible user
@ -156,7 +155,7 @@ spec:
# Setup ansible SSH
mkdir -p /home/ansible/.ssh
printf '%s\n' "$ansible_ssh_pub" > /home/ansible/.ssh/authorized_keys
printf '%s' "$ansible_ssh_pub_b64" | base64 -d > /home/ansible/.ssh/authorized_keys
chmod 600 /home/ansible/.ssh/authorized_keys
chown -R ansible:ansible /home/ansible/.ssh

View File

@ -49,6 +49,20 @@ ensure_tools() {
done
}
sha256_stdin() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum | awk '{print $1}'
return 0
fi
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 | awk '{print $1}'
return 0
fi
cat >/dev/null
echo ""
return 0
}
ensure_namespace() {
if ! kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then
log "Creating namespace '$NAMESPACE' ..."
@ -227,6 +241,54 @@ patch_cnpg_cluster_for_auth() {
fi
}
apply_krb5_conf_mount_to_cnpg() {
if ! kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" >/dev/null 2>&1; then
err "ERROR: Cluster '$CNPG_CLUSTER_NAME' not found in namespace '$NAMESPACE'."
return 1
fi
local conf conf_hash
conf=$(get_krb5_conf)
if [[ -z "$conf" ]]; then
err "Missing krb5.conf data in ConfigMap prole-krb5-conf."
return 1
fi
conf_hash=$(printf '%s' "$conf" | sha256_stdin)
log "Patching CNPG Cluster $CNPG_CLUSTER_NAME to mount krb5.conf (rolling update) ..."
if ! cat <<EOF | kubectl apply --server-side --field-manager=prole-kerberos -f -
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: ${CNPG_CLUSTER_NAME}
namespace: ${NAMESPACE}
spec:
podTemplate:
metadata:
annotations:
prole.dev/krb5-conf-hash: "${conf_hash}"
spec:
volumes:
- name: krb5-conf
configMap:
name: prole-krb5-conf
items:
- key: krb5.conf
path: krb5.conf
containers:
- name: postgres
volumeMounts:
- name: krb5-conf
mountPath: /etc/krb5.conf
subPath: krb5.conf
readOnly: true
EOF
then
err "Note: unable to apply krb5.conf mount via server-side apply."
return 1
fi
}
apply_krb5_conf_to_cnpg_pods() {
local conf
conf=$(get_krb5_conf)
@ -362,8 +424,12 @@ initialize() {
ensure_krb5_conf_configmap
patch_cnpg_cluster_for_auth || true
wait_for_cnpg_pods "$CNPG_WAIT_TIMEOUT"
apply_krb5_conf_to_cnpg_pods
if apply_krb5_conf_mount_to_cnpg; then
wait_for_cnpg_pods "$CNPG_WAIT_TIMEOUT"
else
wait_for_cnpg_pods "$CNPG_WAIT_TIMEOUT"
apply_krb5_conf_to_cnpg_pods
fi
log "Kerberos initialization complete."
}

View File

@ -32,7 +32,9 @@ KRB5_REALM=${KRB5_REALM:-${REALM:-}}
KRB5_KDC=${KRB5_KDC:-}
KRB5_USER=${KRB5_USER:-${KRB5_USERNAME:-}}
KRB5_PASSWORD=${KRB5_PASSWORD:-}
KRB5_TEST_IMAGE=${KRB5_TEST_IMAGE:-${PROLE_KRB_TEST_IMAGE:-}}
KRB5_TEST_IMAGE=${KRB5_TEST_IMAGE:-${PROLE_KRB_TEST_IMAGE:-ubuntu:24.04}}
KRB5_TEST_PACKAGES=${KRB5_TEST_PACKAGES:-"krb5-user krb5-config libpam-krb5 libnss-krb5 adcli samba-common-bin dnsutils ca-certificates"}
REALM_JOIN=${REALM_JOIN:-1}
KEEP_POD=${KEEP_POD:-0}
KRB5_TEST_HOST_NETWORK=${KRB5_TEST_HOST_NETWORK:-0}
KRB5_TEST_DNS_POLICY=${KRB5_TEST_DNS_POLICY:-}
@ -55,40 +57,7 @@ detect_image() {
echo "$KRB5_TEST_IMAGE"
return
fi
local pg_version_file release_file pg_version release
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/conf/postgresql/.version" ]]; then
pg_version_file="$PROLE_HOME/conf/postgresql/.version"
elif [[ -f "$SCRIPT_DIR/../conf/postgresql/.version" ]]; then
pg_version_file="$SCRIPT_DIR/../conf/postgresql/.version"
else
pg_version_file=""
fi
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/prole-db/.version" ]]; then
release_file="$PROLE_HOME/prole-db/.version"
elif [[ -f "$SCRIPT_DIR/../prole-db/.version" ]]; then
release_file="$SCRIPT_DIR/../prole-db/.version"
else
release_file=""
fi
if [[ -n "$pg_version_file" && -f "$pg_version_file" ]]; then
pg_version=$(cat "$pg_version_file" | tr -d '[:space:]')
else
pg_version="17.7"
fi
if [[ -n "$release_file" && -f "$release_file" ]]; then
release=$(cat "$release_file" | tr -d '[:space:]')
else
release="43"
fi
if [[ "$release" =~ ^[0-9]+$ ]]; then
release=$(printf "%03d" "$release")
fi
echo "prole-db:${pg_version}-${release}"
echo "ubuntu:24.04"
}
ensure_configmap() {
@ -124,20 +93,102 @@ ${host_net_block} restartPolicy: Never
- name: kerberos-test
image: ${image}
command: ["sleep","3600"]
volumeMounts:
- name: krb5-conf
mountPath: /etc/krb5.conf
subPath: krb5.conf
volumes:
- name: krb5-conf
configMap:
name: prole-krb5-conf
items:
- key: krb5.conf
path: krb5.conf
EOF
}
install_kerberos_packages() {
local pod_name="$1"
echo "Installing Kerberos client packages in pod..."
kubectl -n "$NAMESPACE" exec "$pod_name" -- env KRB5_TEST_PACKAGES="$KRB5_TEST_PACKAGES" sh -c '
set -e
if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then
if ! grep -q "universe" /etc/apt/sources.list.d/ubuntu.sources; then
awk "
/^Components:/ {
if (\$0 !~ /universe/) {
\$0 = \$0 \" universe\"
}
}
{ print }
" /etc/apt/sources.list.d/ubuntu.sources > /tmp/ubuntu.sources
mv /tmp/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources
fi
elif [ -f /etc/apt/sources.list ]; then
if ! grep -q "universe" /etc/apt/sources.list; then
sed -i "s/ main$/ main universe/; s/ main restricted$/ main restricted universe/; s/ main restricted multiverse$/ main restricted universe multiverse/" /etc/apt/sources.list
fi
fi
apt-get update
available=""
missing=""
for pkg in $KRB5_TEST_PACKAGES; do
if apt-cache show "$pkg" >/dev/null 2>&1; then
available="$available $pkg"
else
missing="$missing $pkg"
fi
done
if [ -n "$missing" ]; then
echo "WARN: skipping missing packages:$missing" >&2
fi
if [ -z "$available" ]; then
echo "ERROR: none of the requested packages are available." >&2
exit 1
fi
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends $available
'
}
resolve_domain() {
local domain="${DOMAIN:-}"
if [[ -z "$domain" ]]; then
domain=$(printf '%s' "$KRB5_REALM" | tr '[:upper:]' '[:lower:]')
fi
printf '%s' "$domain"
}
resolve_host_fqdn() {
local domain="$1"
local host_name="${KRB5_TEST_HOSTNAME:-$2}"
if [[ "$host_name" == *.* ]]; then
printf '%s' "$host_name"
else
printf '%s.%s' "$host_name" "$domain"
fi
}
join_realm() {
local pod_name="$1"
local domain
domain=$(resolve_domain)
local kdc_host
kdc_host=$(printf '%s' "$KRB5_KDC" | awk -F',' '{print $1}' | xargs)
local host_fqdn
host_fqdn=$(resolve_host_fqdn "$domain" "$pod_name")
echo "Registering new Kerberos client as ${host_fqdn} in ${domain} ..."
if kubectl -n "$NAMESPACE" exec "$pod_name" -- sh -c 'command -v adcli >/dev/null 2>&1'; then
printf '%s\n' "$KRB5_PASSWORD" | kubectl -n "$NAMESPACE" exec -i "$pod_name" -- \
adcli join --domain="$domain" --domain-controller "$kdc_host" --login-user "$KRB5_USER" --stdin-password --host-fqdn "$host_fqdn" --show-details
return $?
fi
if kubectl -n "$NAMESPACE" exec "$pod_name" -- sh -c 'command -v realm >/dev/null 2>&1'; then
printf '%s\n' "$KRB5_PASSWORD" | kubectl -n "$NAMESPACE" exec -i "$pod_name" -- \
realm join --user "$KRB5_USER" "$domain"
return $?
fi
echo "ERROR: Neither adcli nor realm found in image; cannot register client." >&2
return 1
}
run_test() {
ensure_tools
ensure_namespace
@ -173,9 +224,20 @@ run_test() {
exit 1
fi
install_kerberos_packages "$pod_name"
echo "Updating /etc/krb5.conf in pod $pod_name from ConfigMap..."
local conf
conf=$(kubectl -n "$NAMESPACE" get configmap prole-krb5-conf -o jsonpath='{.data.krb5\.conf}' 2>/dev/null || true)
if [[ -n "$conf" ]]; then
printf '%s' "$conf" | kubectl -n "$NAMESPACE" exec -i "$pod_name" -- sh -c 'cat > /etc/krb5.conf'
else
echo "WARN: ConfigMap prole-krb5-conf not found; /etc/krb5.conf may be missing or default."
fi
echo "Checking for kinit in pod..."
if ! kubectl -n "$NAMESPACE" exec "$pod_name" -- sh -c 'command -v kinit >/dev/null 2>&1'; then
echo "ERROR: kinit not found in test pod image '$image'." >&2
echo "ERROR: kinit not found after package install in test pod image '$image'." >&2
if [[ "$KEEP_POD" != "1" ]]; then
kubectl -n "$NAMESPACE" delete pod "$pod_name" --ignore-not-found
fi
@ -195,11 +257,12 @@ run_test() {
kubectl -n "$NAMESPACE" exec "$pod_name" -- klist || true
if [[ "${REALM_JOIN:-0}" == "1" ]]; then
echo "Attempting realm join inside test pod..."
if kubectl -n "$NAMESPACE" exec "$pod_name" -- sh -c 'command -v realm >/dev/null 2>&1'; then
printf '%s\n' "$KRB5_PASSWORD" | kubectl -n "$NAMESPACE" exec -i "$pod_name" -- realm join --user "$KRB5_USER" "$KRB5_REALM" || true
else
echo "realm command not found in image; skipping realm join."
if ! join_realm "$pod_name"; then
echo "ERROR: realm join failed." >&2
if [[ "$KEEP_POD" != "1" ]]; then
kubectl -n "$NAMESPACE" delete pod "$pod_name" --ignore-not-found
fi
exit 1
fi
fi

View File

@ -18,11 +18,12 @@ CONFIG_FILE="$PROLE_HOME/conf/port-mappings.properties"
usage() {
cat <<EOF
Usage:
$PROG [-v|--verbose] [-c|--config-file=FILE] <start|stop|restart|status> [component]
$PROG [-v|--verbose] [-f|--force] [-c|--config-file=FILE] <start|stop|restart|status> [component]
Options:
-c, --config-file=FILE Path to local-ports.properties (XML)
-v, --verbose Verbose output
-f, --force Force: kill existing processes blocking ports
Examples:
$PROG -c ./port-mappings.properties start
@ -32,6 +33,7 @@ EOF
}
TARGET_ID=""
FORCE=0
log() { printf '%s\n' "$*"; }
vlog() { [ "$VERBOSE" -eq 1 ] && printf '[verbose] %s\n' "$*" || true; }
@ -129,16 +131,11 @@ if [ -z "${PF_MANAGEMENT_NAMESPACE:-}" ] && [ -n "${NAMESPACE:-}" ]; then
PF_MANAGEMENT_NAMESPACE="$NAMESPACE"
fi
SUPABASE_NAMESPACE="${SUPABASE_NAMESPACE:-supabase}"
SUPABASE_DB_TARGET="${SUPABASE_DB_TARGET:-${SUPABASE_DB_SERVICE:-svc/db}}"
# SUPABASE_NAMESPACE="${SUPABASE_NAMESPACE:-supabase}"
# SUPABASE_DB_TARGET="${SUPABASE_DB_TARGET:-${SUPABASE_DB_SERVICE:-svc/db}}"
# Supabase logic removed as it's handled in another script
SUPABASE_ENABLED_EFFECTIVE=0
if is_truthy "${SUPABASE_ENABLED:-}"; then SUPABASE_ENABLED_EFFECTIVE=1; fi
if is_truthy "${init_cluster_supabase_enabled:-}"; then SUPABASE_ENABLED_EFFECTIVE=1; fi
if [ "$SUPABASE_ENABLED_EFFECTIVE" -eq 0 ]; then
if is_truthy "$(cfg_value "SUPABASE_ENABLED")"; then SUPABASE_ENABLED_EFFECTIVE=1; fi
if is_truthy "$(cfg_value "init_cluster.supabase_enabled")"; then SUPABASE_ENABLED_EFFECTIVE=1; fi
fi
PROLE_DB_ALT_PORT_BASE="${PROLE_DB_ALT_PORT_BASE:-15432}"
PROLE_DB_ALT_PORT_EFFECTIVE=""
@ -415,43 +412,16 @@ foreach_mapping() {
ns="${ns//\$\{PROLE_MANAGEMENT_NAMESPACE\}/$PF_MANAGEMENT_NAMESPACE}"
fi
# Mapping-specific overrides
case "$id" in
prometheus|grafana)
if [ -n "${PF_MONITORING_NAMESPACE:-}" ]; then
ns="$PF_MONITORING_NAMESPACE"
fi
;;
openbao)
if [ -n "${PF_MANAGEMENT_NAMESPACE:-}" ]; then
ns="$PF_MANAGEMENT_NAMESPACE"
fi
;;
esac
# Mapping-specific overrides (removed hardcoded namespace overrides)
if [ "$id" = "postgres" ]; then
if [ -n "${DB_HOST_PORT:-}" ]; then
hostPort="$DB_HOST_PORT"
fi
if [ "$SUPABASE_ENABLED_EFFECTIVE" -eq 1 ]; then
ns="$SUPABASE_NAMESPACE"
target="$SUPABASE_DB_TARGET"
fi
# Supabase override removed
fi
if [ "$id" = "prole-db" ]; then
if [ "$SUPABASE_ENABLED_EFFECTIVE" -ne 1 ]; then
in_mapping=0
buffer=""
continue
fi
if [ -n "${NAMESPACE:-}" ]; then
ns="$NAMESPACE"
fi
if [ -n "${PROLE_DB_ALT_PORT_EFFECTIVE:-}" ]; then
hostPort="$PROLE_DB_ALT_PORT_EFFECTIVE"
fi
fi
# prole-db logic removed, now uses XML
# Basic validation
if [ -z "$id" ] || [ -z "$ns" ] || [ -z "$target" ] || [ -z "$hostPort" ] || [ -z "$servicePort" ]; then
@ -584,11 +554,61 @@ status_one() {
fi
}
scan_for_collisions() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
if port_in_use "$hostPort"; then
# find which process is using it
local pid_info=""
if have lsof; then
pid_info=$(lsof -nP -iTCP:"$hostPort" -sTCP:LISTEN -t 2>/dev/null | head -n 1)
fi
if [ -n "$pid_info" ]; then
# Check if this PID is already managed by us
local pidfile pid_managed
pidfile="$(pid_file_for "$id")"
pid_managed="$(read_pid "$pidfile" 2>/dev/null || true)"
if [ "$pid_info" = "$pid_managed" ]; then
vlog "Port $hostPort is in use by our own process ($id, pid=$pid_info). This is fine for start/restart."
return 0
fi
local proc_details
proc_details=$(ps -p "$pid_info" -o pid=,command= 2>/dev/null | sed 's/[[:space:]]\+/ /g' || echo "$pid_info")
if [ "$FORCE" -eq 1 ]; then
log "Port $hostPort is in use by: $proc_details"
log "Force enabled. Killing process $pid_info..."
if ! kill -9 "$pid_info" 2>/dev/null; then
err "Failed to kill process $pid_info. Permission denied?"
exit 1
fi
sleep 0.5
else
err "Port collision detected: Port $hostPort is already in use by another process."
err "Process details: $proc_details"
err "Use -f or --force to kill the offending process, or stop it manually."
exit 1
fi
else
# Port in use but we can't find PID (maybe another user's process)
err "Port collision detected: Port $hostPort is in use, but could not determine PID (check with sudo lsof -i :$hostPort)."
exit 1
fi
fi
}
do_start() {
validate_env
# Preflight: require Docker daemon and k3d (if applicable)
ensure_docker_running
ensure_k3d_ready_if_applicable
vlog "Scanning for port collisions..."
foreach_mapping scan_for_collisions
foreach_mapping start_port_forward
}
@ -603,7 +623,11 @@ do_restart() {
# Preflight: require Docker daemon and k3d (if applicable)
ensure_docker_running
ensure_k3d_ready_if_applicable
vlog "Scanning for port collisions (excluding our own)..."
# During restart, we'll stop them first anyway, but let's be safe.
foreach_mapping stop_port_forward
foreach_mapping scan_for_collisions
foreach_mapping start_port_forward
}
@ -682,6 +706,10 @@ while [ $# -gt 0 ]; do
VERBOSE=1
shift
;;
-f|--force)
FORCE=1
shift
;;
-c)
shift
[ $# -gt 0 ] || { err "-c requires a file path"; usage; exit 2; }

View File

@ -1,850 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# init_supabase.sh
# Purpose:
# - Deploy Supabase stack in Kubernetes using the official supabase/docker compose
# - Rewire Supabase to use the CNPG Postgres service
# - Optionally stage images for k3d and set imagePullPolicy
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Load environment and config via prole_cfg.sh
# shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh"
ACTION=${1:-}
CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-prole-db}
SUPABASE_HOME=${SUPABASE_HOME:-}
SUPABASE_USE_DEV_COMPOSE=${SUPABASE_USE_DEV_COMPOSE:-0}
SUPABASE_IMAGE_PULL_POLICY=${SUPABASE_IMAGE_PULL_POLICY:-IfNotPresent}
SUPABASE_K8S_DIR=${SUPABASE_K8S_DIR:-${PROLE_HOME:-$SCRIPT_DIR/..}/build/supabase-k8s}
SUPABASE_POSTGRES_HOST=${SUPABASE_POSTGRES_HOST:-db}
SUPABASE_POSTGRES_DB=${SUPABASE_POSTGRES_DB:-postgres}
SUPABASE_POSTGRES_PORT=${SUPABASE_POSTGRES_PORT:-5432}
SUPABASE_APPLY_DB_MIGRATIONS=${SUPABASE_APPLY_DB_MIGRATIONS:-1}
SUPABASE_BOOTSTRAP_MODE=${SUPABASE_BOOTSTRAP_MODE:-prole}
SUPABASE_STAGE_IMAGES=${SUPABASE_STAGE_IMAGES:-0}
SUPABASE_IMAGE_STAGE_METHOD=${SUPABASE_IMAGE_STAGE_METHOD:-registry}
SUPABASE_IMAGE_REGISTRY=${SUPABASE_IMAGE_REGISTRY:-}
SUPABASE_IMAGE_REGISTRY_PUSH=${SUPABASE_IMAGE_REGISTRY_PUSH:-}
SUPABASE_IMAGE_REGISTRY_PORT=${SUPABASE_IMAGE_REGISTRY_PORT:-5000}
SUPABASE_IMAGE_REGISTRY_NAME=${SUPABASE_IMAGE_REGISTRY_NAME:-}
SUPABASE_IMAGE_REGISTRY_CONFIGURE=${SUPABASE_IMAGE_REGISTRY_CONFIGURE:-1}
SUPABASE_IMAGE_PLATFORM=${SUPABASE_IMAGE_PLATFORM:-}
usage() {
cat <<EOF
Usage: $0 [start|stop|restart|status]
Actions:
start Convert Supabase docker-compose to K8s and apply in namespace
stop Delete Supabase K8s resources from namespace
restart Re-apply Supabase manifests
status Show Supabase resources
Env overrides:
SUPABASE_HOME, SUPABASE_USE_DEV_COMPOSE, SUPABASE_K8S_DIR
SUPABASE_BOOTSTRAP_MODE (prole | clean | clean+prole)
SUPABASE_STAGE_IMAGES (0|1), SUPABASE_IMAGE_STAGE_METHOD (registry|import)
SUPABASE_IMAGE_REGISTRY, SUPABASE_IMAGE_REGISTRY_PUSH, SUPABASE_IMAGE_REGISTRY_NAME
SUPABASE_IMAGE_REGISTRY_PORT, SUPABASE_IMAGE_REGISTRY_CONFIGURE, SUPABASE_IMAGE_PLATFORM
SUPABASE_IMAGE_PULL_POLICY (default: IfNotPresent)
SUPABASE_POSTGRES_HOST (default: db)
SUPABASE_POSTGRES_DB (default: postgres)
SUPABASE_POSTGRES_PORT (default: 5432)
SUPABASE_APPLY_DB_MIGRATIONS (default: 1)
EOF
exit 1
}
log() { printf '%s\n' "$*"; }
err() { printf '%s\n' "$*" >&2; }
require_cmd() {
command -v "$1" >/dev/null || { err "Missing required tool: $1"; exit 1; }
}
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
err "Docker Compose not found (expected 'docker compose' or 'docker-compose')"
exit 1
}
is_truthy() {
case "${1:-}" in
1|true|TRUE|True|yes|YES|Yes|y|Y) return 0 ;;
*) return 1 ;;
esac
}
needs_prole_db() {
case "$SUPABASE_BOOTSTRAP_MODE" in
prole|clean+prole|clean-prole|clean_then_prole) return 0 ;;
*) return 1 ;;
esac
}
get_k3d_cluster_name() {
printf 'prole-%s-cluster' "${CLUSTER_ENV:-dev}"
}
detect_cluster_platform() {
local arch os
arch=$(kubectl get nodes -o jsonpath='{.items[0].status.nodeInfo.architecture}' 2>/dev/null || true)
os=$(kubectl get nodes -o jsonpath='{.items[0].status.nodeInfo.operatingSystem}' 2>/dev/null || true)
if [[ -z "$arch" ]]; then
arch=$(docker info --format '{{.Architecture}}' 2>/dev/null || true)
fi
if [[ -z "$os" ]]; then
os="linux"
fi
if [[ -n "$arch" ]]; then
printf '%s/%s' "$os" "$arch"
fi
}
SUPABASE_IMAGES_STAGED=0
ensure_tools() {
for t in kubectl kompose python3 sed awk base64; do
command -v "$t" >/dev/null || { err "Missing required tool: $t"; exit 1; }
done
}
ensure_namespace() {
if ! kubectl get namespace "supabase" >/dev/null 2>&1; then
err "ERROR: namespace 'supabase' not found. Supabase must be deployed in its own namespace."
exit 1
fi
}
ensure_prereqs() {
local require_prole_db="${1:-1}"
ensure_namespace
# We still want to ensure prole-db-superuser secret is in the CURRENT namespace (where prole-db is)
# but supabase itself will be in 'supabase' namespace.
# The issue description says: "connect supabase postgres network ports to our new namespace"
# This implies we might need to create services in the CURRENT namespace that point to supabase.
if [[ "$require_prole_db" == "1" ]]; then
if ! kubectl get secret prole-db-superuser -n "$NAMESPACE" >/dev/null 2>&1; then
err "ERROR: Secret 'prole-db-superuser' not found in namespace '$NAMESPACE'."
exit 1
fi
fi
}
detect_supabase_home() {
if [[ -n "$SUPABASE_HOME" && -d "$SUPABASE_HOME" ]]; then
echo "$SUPABASE_HOME"
return 0
fi
if [[ -d "$HOME/prole/supabase" ]]; then
echo "$HOME/prole/supabase"
return 0
fi
if [[ -d "$HOME/dev/supabase" ]]; then
echo "$HOME/dev/supabase"
return 0
fi
if [[ -n "${PROLE_HOME:-}" && -d "$PROLE_HOME/../supabase" ]]; then
echo "$PROLE_HOME/../supabase"
return 0
fi
return 1
}
get_db_password() {
kubectl -n "$NAMESPACE" get secret prole-db-superuser -o jsonpath='{.data.password}' 2>/dev/null | base64 -d
}
set_env_kv() {
local file="$1" key="$2" value="$3"
# Ensure value is quoted if it contains spaces and is not already quoted
if [[ "$value" == *" "* && ! "$value" =~ ^\".*\"$ && ! "$value" =~ ^\'.*\'$ ]]; then
value="\"$value\""
fi
if grep -q "^${key}=" "$file" 2>/dev/null; then
sed -i.bak "s|^${key}=.*|${key}=${value}|" "$file"
else
printf "%s=%s\n" "$key" "$value" >> "$file"
fi
}
build_env_file() {
local supa_home="$1"
local override_db="${2:-1}"
local docker_dir="$supa_home/docker"
local env_base="$docker_dir/.env"
local env_example="$docker_dir/.env.example"
local env_out="$SUPABASE_K8S_DIR/.env"
local db_pass
db_pass=$(get_db_password || true)
mkdir -p "$SUPABASE_K8S_DIR"
if [[ -f "$env_base" ]]; then
cp "$env_base" "$env_out"
elif [[ -f "$env_example" ]]; then
cp "$env_example" "$env_out"
else
err "ERROR: Supabase .env or .env.example not found in $docker_dir"
exit 1
fi
# If we used .env.example, generate required secrets
if [[ ! -f "$env_base" ]]; then
if [[ -x "$docker_dir/utils/generate-keys.sh" ]]; then
log "Generating Supabase secrets from utils/generate-keys.sh ..."
local gen_out
gen_out=$(bash "$docker_dir/utils/generate-keys.sh" </dev/null || true)
while IFS= read -r line; do
[[ "$line" =~ ^[A-Z0-9_]+= ]] || continue
local k="${line%%=*}"
local v="${line#*=}"
set_env_kv "$env_out" "$k" "$v"
done <<< "$gen_out"
fi
fi
if [[ "$override_db" == "1" ]]; then
if [[ -n "$db_pass" ]]; then
set_env_kv "$env_out" "POSTGRES_PASSWORD" "$db_pass"
fi
set_env_kv "$env_out" "POSTGRES_HOST" "$SUPABASE_POSTGRES_HOST"
set_env_kv "$env_out" "POSTGRES_DB" "$SUPABASE_POSTGRES_DB"
set_env_kv "$env_out" "POSTGRES_PORT" "$SUPABASE_POSTGRES_PORT"
fi
}
ensure_local_image() {
local img="$1"
local platform="${2:-}"
local pull_args=()
if [[ -n "$platform" ]]; then
pull_args+=(--platform "$platform")
fi
if docker image inspect "$img" >/dev/null 2>&1; then
docker pull "${pull_args[@]}" "$img" >/dev/null 2>&1 || true
return 0
fi
docker pull "${pull_args[@]}" "$img"
}
get_compose_images() {
local docker_dir="$1"
shift
local files=("$@")
local compose
compose="$(compose_cmd)"
(
cd "$docker_dir" && \
$compose "${files[@]}" --env-file "$SUPABASE_K8S_DIR/.env" config --images
) | awk 'NF' | sort -u
}
ensure_k3d_registry() {
local cluster_name network_name registry_name host_port
cluster_name="$(get_k3d_cluster_name)"
network_name="k3d-${cluster_name}"
if [[ -n "$SUPABASE_IMAGE_REGISTRY_NAME" ]]; then
registry_name="$SUPABASE_IMAGE_REGISTRY_NAME"
elif docker inspect k3d-prole-registry >/dev/null 2>&1; then
registry_name="k3d-prole-registry"
else
registry_name="k3d-${cluster_name}-registry"
fi
if ! docker inspect "$registry_name" >/dev/null 2>&1; then
if docker network inspect "$network_name" >/dev/null 2>&1; then
log "Creating k3d registry '$registry_name' on network '$network_name'..."
k3d registry create "$registry_name" --port "$SUPABASE_IMAGE_REGISTRY_PORT" --default-network "$network_name"
else
log "Creating k3d registry '$registry_name' on default network..."
k3d registry create "$registry_name" --port "$SUPABASE_IMAGE_REGISTRY_PORT"
fi
fi
if docker network inspect "$network_name" >/dev/null 2>&1; then
if ! docker inspect -f '{{json .NetworkSettings.Networks}}' "$registry_name" | grep -q "\"$network_name\""; then
log "Connecting registry '$registry_name' to network '$network_name'..."
docker network connect "$network_name" "$registry_name" >/dev/null 2>&1 || true
fi
fi
host_port=$(docker port "$registry_name" 5000/tcp 2>/dev/null | awk -F: 'NR==1 {print $2}')
if [[ -z "$host_port" ]]; then
host_port="$SUPABASE_IMAGE_REGISTRY_PORT"
fi
if [[ -z "$SUPABASE_IMAGE_REGISTRY" ]]; then
SUPABASE_IMAGE_REGISTRY="${registry_name}:5000"
fi
if [[ -z "$SUPABASE_IMAGE_REGISTRY_PUSH" ]]; then
SUPABASE_IMAGE_REGISTRY_PUSH="localhost:${host_port}"
fi
export SUPABASE_IMAGE_REGISTRY SUPABASE_IMAGE_REGISTRY_PUSH
}
configure_k3d_registry() {
local registry="$1"
local cluster_name desired nodes changed existing
if ! is_truthy "$SUPABASE_IMAGE_REGISTRY_CONFIGURE"; then
return 0
fi
cluster_name="$(get_k3d_cluster_name)"
nodes=$(k3d node list | awk -v c="$cluster_name" 'NR>1 && $3==c && ($2=="server" || $2=="agent") {print $1}')
if [[ -z "$nodes" ]]; then
return 0
fi
desired=$(cat <<EOF
mirrors:
"$registry":
endpoint:
- "http://$registry"
configs:
"$registry":
tls:
insecure_skip_verify: true
EOF
)
desired=$(printf '%s' "$desired")
changed=0
for node in $nodes; do
existing=$(docker exec "$node" sh -c "cat /etc/rancher/k3s/registries.yaml 2>/dev/null" || true)
existing=$(printf '%s' "$existing")
if [[ "$existing" != "$desired" ]]; then
log "Configuring registry mirror on $node..."
printf '%s\n' "$desired" | docker exec -i "$node" sh -c "mkdir -p /etc/rancher/k3s && cat > /etc/rancher/k3s/registries.yaml"
changed=1
fi
done
if [[ "$changed" == "1" ]]; then
log "Restarting k3d nodes to apply registry config..."
for node in $nodes; do
docker restart "$node" >/dev/null
done
kubectl wait --for=condition=Ready node --all --timeout=180s >/dev/null 2>&1 || true
fi
}
stage_images_if_needed() {
local docker_dir="$1"
shift
local files=("$@")
if [[ "$SUPABASE_STAGE_IMAGES" != "1" ]]; then
return 0
fi
if [[ "$SUPABASE_IMAGES_STAGED" == "1" ]]; then
return 0
fi
require_cmd docker
local platform
if [[ -n "$SUPABASE_IMAGE_PLATFORM" ]]; then
platform="$SUPABASE_IMAGE_PLATFORM"
else
platform="$(detect_cluster_platform || true)"
fi
local images
images=$(get_compose_images "$docker_dir" "${files[@]}")
if [[ -z "$images" ]]; then
err "WARN: No images found in Supabase compose config."
return 0
fi
case "$SUPABASE_IMAGE_STAGE_METHOD" in
registry)
require_cmd k3d
ensure_k3d_registry
configure_k3d_registry "$SUPABASE_IMAGE_REGISTRY"
if [[ -n "$platform" ]]; then
log "Staging Supabase images into registry '$SUPABASE_IMAGE_REGISTRY_PUSH' for platform '$platform'..."
else
log "Staging Supabase images into registry '$SUPABASE_IMAGE_REGISTRY_PUSH'..."
fi
for img in $images; do
local push_ref="${SUPABASE_IMAGE_REGISTRY_PUSH}/${img}"
local media_type
log "Staging $img -> $push_ref"
ensure_local_image "$img" "$platform"
media_type=$(docker image inspect "$img" --format '{{.Descriptor.mediaType}}' 2>/dev/null || true)
if [[ "$media_type" == *"manifest.list"* || "$media_type" == *"image.index"* ]]; then
if docker buildx version >/dev/null 2>&1; then
log "Publishing multi-platform image via buildx imagetools: $img"
if [[ -n "$platform" ]]; then
docker buildx imagetools create --tag "$push_ref" --platform "$platform" "$img"
else
docker buildx imagetools create --tag "$push_ref" "$img"
fi
continue
fi
fi
docker tag "$img" "$push_ref"
if [[ -n "$platform" ]]; then
docker push --platform "$platform" "$push_ref"
else
docker push "$push_ref"
fi
done
;;
import)
require_cmd k3d
local cluster_name
cluster_name="$(get_k3d_cluster_name)"
log "Importing Supabase images into k3d cluster '$cluster_name'..."
for img in $images; do
ensure_local_image "$img" "$platform"
k3d image import --mode=direct "$img" -c "$cluster_name"
done
;;
*)
err "ERROR: Unknown SUPABASE_IMAGE_STAGE_METHOD: $SUPABASE_IMAGE_STAGE_METHOD"
exit 1
;;
esac
SUPABASE_IMAGES_STAGED=1
}
convert_compose_to_k8s() {
local supa_home="$1"
local mode="${2:-prole}"
local docker_dir="$supa_home/docker"
local compose_file="$docker_dir/docker-compose.yml"
local dev_compose="$docker_dir/dev/docker-compose.dev.yml"
if [[ ! -f "$compose_file" ]]; then
err "ERROR: docker-compose.yml not found at $compose_file"
exit 1
fi
rm -rf "$SUPABASE_K8S_DIR"
mkdir -p "$SUPABASE_K8S_DIR"
local override_db="0"
if [[ "$mode" == "prole" ]]; then
override_db="1"
fi
build_env_file "$supa_home" "$override_db"
# Export .env for kompose interpolation
set -a
# Fix unquoted values with spaces in the .env file before sourcing
# Use python for robust .env parsing and quoting
python3 - <<PY
import pathlib
import re
env_file = pathlib.Path("$SUPABASE_K8S_DIR/.env")
if env_file.exists():
content = env_file.read_text()
new_lines = []
for line in content.splitlines():
# Match KEY=VALUE where VALUE has spaces and is not quoted
m = re.match(r'^([A-Z0-9_]+)=([^"\'].* .*)$', line)
if m:
key, val = m.groups()
new_lines.append(f'{key}="{val}"')
else:
new_lines.append(line)
env_file.write_text("\n".join(new_lines) + "\n")
PY
# shellcheck disable=SC1090
source "$SUPABASE_K8S_DIR/.env"
set +a
local files=("-f" "$compose_file")
if [[ "$SUPABASE_USE_DEV_COMPOSE" == "1" && -f "$dev_compose" ]]; then
files+=("-f" "$dev_compose")
fi
stage_images_if_needed "$docker_dir" "${files[@]}"
log "Converting Supabase docker-compose to Kubernetes manifests for namespace 'supabase' ..."
kompose "${files[@]}" convert -n "supabase" -o "$SUPABASE_K8S_DIR" --volumes=configMap --suppress-warnings
if [[ "$mode" == "prole" ]]; then
# Remove the built-in Supabase DB workloads (we use CNPG instead)
rm -f "$SUPABASE_K8S_DIR"/db-*.yaml "$SUPABASE_K8S_DIR"/db*.yaml 2>/dev/null || true
fi
# Enforce imagePullPolicy
python3 <<PY
import pathlib
import re
k8s_dir = pathlib.Path("$SUPABASE_K8S_DIR")
policy = "$SUPABASE_IMAGE_PULL_POLICY"
registry = "$SUPABASE_IMAGE_REGISTRY" if "$SUPABASE_IMAGE_STAGE_METHOD" == "registry" else ""
print(f"DEBUG: Processing directory {k8s_dir}")
def patch_file(path: pathlib.Path):
lines = path.read_text().splitlines()
out = []
for line in lines:
m = re.match(r'^(\s*)image:\s*(\S+)', line)
if m:
indent, image = m.groups()
if registry and not image.startswith(f"{registry}/"):
image = f"{registry}/{image}"
out.append(f"{indent}image: {image}")
out.append(f"{indent}imagePullPolicy: {policy}")
else:
out.append(line)
path.write_text("\\n".join(out) + "\\n")
# Sanitize container names (must not contain dots)
def sanitize_container_names(path: pathlib.Path):
content = path.read_text()
lines = content.splitlines()
new_lines = []
changed = False
for line in lines:
# Target name: or - name:
m = re.match(r'^(\s*)((?:-\s+)?name:\s+)([a-z0-9.-]+)$', line)
if m:
indent, prefix, name = m.groups()
if '.' in name:
sanitized = name.replace('.', '-')
line = f"{indent}{prefix}{sanitized}"
changed = True
new_lines.append(line)
if changed:
path.write_text("\n".join(new_lines) + "\n")
for path in k8s_dir.glob("*.yaml"):
patch_file(path)
sanitize_container_names(path)
PY
}
delete_supabase_db_resources() {
kubectl delete -n "supabase" deploy,sts,svc,pvc -l io.kompose.service=db --ignore-not-found >/dev/null 2>&1 || true
kubectl delete -n "supabase" deploy/db statefulset/db svc/db pvc/db --ignore-not-found >/dev/null 2>&1 || true
}
rollout_restart_supabase() {
kubectl rollout restart -n "supabase" deployment >/dev/null 2>&1 || true
}
apply_k8s_resources() {
local mode="${1:-prole}"
log "Applying Supabase resources to namespace 'supabase' ..."
if [[ "$mode" == "clean" ]]; then
delete_supabase_db_resources
kubectl delete -n "$NAMESPACE" svc/supabase-db --ignore-not-found >/dev/null 2>&1 || true
elif [[ "$mode" == "prole" ]]; then
delete_supabase_db_resources
fi
kubectl apply -n "supabase" -f "$SUPABASE_K8S_DIR"
if [[ "$mode" != "prole" ]]; then
return 0
fi
# Create an alias service 'supabase-db' in the CURRENT namespace that points to Supabase Postgres in 'supabase' namespace
# This allows prole-db (in current namespace) to connect to Supabase
cat <<EOF | kubectl apply -n "$NAMESPACE" -f -
apiVersion: v1
kind: Service
metadata:
name: supabase-db
labels:
app: supabase-db-federated
spec:
type: ExternalName
externalName: db.supabase.svc.cluster.local
EOF
# Also create an alias 'db' in 'supabase' namespace pointing to prole-db in CURRENT namespace
# This allows Supabase components to use prole-db as their primary DB
# We use port 5432 because Supabase expects its postgres on that port.
cat <<EOF | kubectl apply -n "supabase" -f -
apiVersion: v1
kind: Service
metadata:
name: db
labels:
app: prole-db-alias
spec:
type: ExternalName
externalName: prole-db-rw.${NAMESPACE}.svc.cluster.local
EOF
rollout_restart_supabase
}
apply_db_migrations() {
if [[ "$SUPABASE_APPLY_DB_MIGRATIONS" != "1" ]]; then
log "SUPABASE_APPLY_DB_MIGRATIONS=0; skipping Supabase DB migrations."
return
fi
local supa_home="$1"
local sql_dir="$supa_home/docker/volumes/db"
if [[ ! -d "$sql_dir" ]]; then
err "WARN: Supabase SQL directory not found: $sql_dir"
return
fi
local primary
primary=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{.status.currentPrimary}' 2>/dev/null || true)
if [[ -z "$primary" ]]; then
primary=$(kubectl -n "$NAMESPACE" get pods -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
fi
if [[ -z "$primary" ]]; then
err "WARN: Unable to locate primary CNPG pod; skipping migrations."
return
fi
local db_pass
db_pass=$(get_db_password || true)
if [[ -z "$db_pass" ]]; then
err "WARN: Unable to read prole-db-superuser password; skipping migrations."
return
fi
log "Applying Supabase SQL migrations to CNPG (${SUPABASE_POSTGRES_DB}) ..."
# We need to pre-create roles that Supabase SQL scripts expect.
# Also create _supabase database if it doesn't exist.
local db_user
db_user=$(kubectl -n "$NAMESPACE" get secret prole-db-user -o jsonpath='{.data.username}' 2>/dev/null | base64 -d || echo "prole")
log "Pre-creating Supabase roles and database..."
kubectl -n "$NAMESPACE" exec -i "$primary" -c postgres -- \
env PGPASSWORD="$db_pass" psql -U postgres -d postgres -v ON_ERROR_STOP=1 <<EOF
DO \$\$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticator') THEN
CREATE ROLE authenticator NOINHERIT LOGIN PASSWORD '$db_pass';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN
CREATE ROLE anon NOLOGIN;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated') THEN
CREATE ROLE authenticated NOLOGIN;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN
CREATE ROLE service_role NOLOGIN;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_admin') THEN
CREATE ROLE supabase_admin WITH LOGIN SUPERUSER PASSWORD '$db_pass';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_auth_admin') THEN
CREATE ROLE supabase_auth_admin WITH LOGIN PASSWORD '$db_pass';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_storage_admin') THEN
CREATE ROLE supabase_storage_admin WITH LOGIN PASSWORD '$db_pass';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'pgbouncer') THEN
CREATE ROLE pgbouncer WITH LOGIN PASSWORD '$db_pass';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_functions_admin') THEN
CREATE ROLE supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION PASSWORD '$db_pass';
END IF;
-- Storage and Auth admins need to be able to login and perform migrations
EXECUTE 'ALTER ROLE supabase_auth_admin WITH LOGIN PASSWORD ' || quote_literal('$db_pass');
EXECUTE 'ALTER ROLE supabase_storage_admin WITH LOGIN PASSWORD ' || quote_literal('$db_pass');
EXECUTE 'ALTER ROLE supabase_functions_admin WITH LOGIN PASSWORD ' || quote_literal('$db_pass');
EXECUTE 'ALTER ROLE authenticator WITH LOGIN PASSWORD ' || quote_literal('$db_pass');
EXECUTE 'ALTER ROLE pgbouncer WITH LOGIN PASSWORD ' || quote_literal('$db_pass');
EXECUTE 'ALTER ROLE supabase_admin WITH LOGIN PASSWORD ' || quote_literal('$db_pass');
END
\$\$;
SELECT 'CREATE DATABASE $SUPABASE_POSTGRES_DB OWNER $db_user'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '$SUPABASE_POSTGRES_DB')\gexec
\c $SUPABASE_POSTGRES_DB
DO \$\$
BEGIN
-- Give them permissions to create schemas/tables in their respective databases
EXECUTE 'GRANT ALL ON DATABASE ' || quote_ident(current_database()) || ' TO supabase_auth_admin';
EXECUTE 'GRANT ALL ON DATABASE ' || quote_ident(current_database()) || ' TO supabase_storage_admin';
EXECUTE 'GRANT ALL ON DATABASE ' || quote_ident(current_database()) || ' TO supabase_admin';
EXECUTE 'GRANT ALL ON DATABASE ' || quote_ident(current_database()) || ' TO postgres';
EXECUTE 'GRANT ALL ON DATABASE ' || quote_ident(current_database()) || ' TO ' || quote_ident('$db_user');
IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'extensions') THEN
CREATE SCHEMA extensions;
END IF;
-- Pre-create Supabase schemas
IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'auth') THEN
CREATE SCHEMA auth;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'storage') THEN
CREATE SCHEMA storage;
END IF;
-- Ensure public schema is writable (PG15+ default is restricted)
GRANT ALL ON SCHEMA public TO public;
GRANT ALL ON SCHEMA public TO postgres;
GRANT ALL ON SCHEMA public TO anon, authenticated, service_role, supabase_admin;
-- Ensure other schemas are accessible
GRANT ALL ON SCHEMA extensions TO public;
GRANT ALL ON SCHEMA extensions TO postgres;
GRANT ALL ON SCHEMA extensions TO anon, authenticated, service_role, supabase_admin;
GRANT ALL ON SCHEMA auth TO supabase_auth_admin, supabase_admin, postgres;
GRANT ALL ON SCHEMA storage TO supabase_storage_admin, supabase_admin, postgres;
-- Role memberships
IF NOT EXISTS (SELECT 1 FROM pg_auth_members m JOIN pg_roles r1 ON m.member = r1.oid JOIN pg_roles r2 ON m.roleid = r2.oid WHERE r1.rolname = 'anon' AND r2.rolname = 'authenticator') THEN
GRANT anon TO authenticator;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_auth_members m JOIN pg_roles r1 ON m.member = r1.oid JOIN pg_roles r2 ON m.roleid = r2.oid WHERE r1.rolname = 'authenticated' AND r2.rolname = 'authenticator') THEN
GRANT authenticated TO authenticator;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_auth_members m JOIN pg_roles r1 ON m.member = r1.oid JOIN pg_roles r2 ON m.roleid = r2.oid WHERE r1.rolname = 'service_role' AND r2.rolname = 'authenticator') THEN
GRANT service_role TO authenticator;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_auth_members m JOIN pg_roles r1 ON m.member = r1.oid JOIN pg_roles r2 ON m.roleid = r2.oid WHERE r1.rolname = 'postgres' AND r2.rolname = 'supabase_admin') THEN
GRANT postgres TO supabase_admin;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_auth_members m JOIN pg_roles r1 ON m.member = r1.oid JOIN pg_roles r2 ON m.roleid = r2.oid WHERE r1.rolname = 'supabase_admin' AND r2.rolname = 'postgres') THEN
GRANT supabase_admin TO postgres;
END IF;
END
\$\$;
SELECT 'CREATE DATABASE _supabase OWNER $db_user'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '_supabase')\gexec
EOF
local files=(
"$sql_dir/_supabase.sql"
"$sql_dir/webhooks.sql"
"$sql_dir/realtime.sql"
"$sql_dir/logs.sql"
"$sql_dir/pooler.sql"
"$sql_dir/jwt.sql"
"$sql_dir/roles.sql"
)
for f in "${files[@]}"; do
if [[ -f "$f" ]]; then
log "Applying $(basename "$f") ..."
# Pass variables to psql. Some scripts use :pguser and :pgpass
# We use POSTGRES_USER and POSTGRES_PASSWORD env vars for the backticks in SQL scripts
# We also use sed to comment out the CREATE EXTENSION pg_net line if it's there
# We also ensure the database exists and the user has permissions on it
kubectl -n "$NAMESPACE" exec -i "$primary" -c postgres -- \
env PGPASSWORD="$db_pass" POSTGRES_USER="$db_user" POSTGRES_PASSWORD="$db_pass" \
bash -c "sed 's/CREATE EXTENSION IF NOT EXISTS pg_net/-- CREATE EXTENSION IF NOT EXISTS pg_net/' | psql -U postgres -d '$SUPABASE_POSTGRES_DB' -v ON_ERROR_STOP=1 --set=pguser='$db_user' --set=pgpass='$db_pass' -c 'GRANT ALL ON DATABASE \"$SUPABASE_POSTGRES_DB\" TO postgres; GRANT ALL ON DATABASE \"$SUPABASE_POSTGRES_DB\" TO supabase_admin; GRANT ALL ON SCHEMA public TO postgres; GRANT ALL ON SCHEMA public TO supabase_admin;' -f -" \
< "$f" || {
err "Failed to apply $(basename "$f"), but continuing..."
}
fi
done
}
delete_k8s_resources() {
if [[ -d "$SUPABASE_K8S_DIR" ]]; then
kubectl delete -n "supabase" -f "$SUPABASE_K8S_DIR" --ignore-not-found || true
fi
kubectl delete -n "$NAMESPACE" svc/supabase-db --ignore-not-found || true
kubectl delete -n "supabase" svc/db --ignore-not-found || true
delete_supabase_db_resources
}
status() {
ensure_tools
log "Supabase status in namespace 'supabase':"
kubectl get deploy,svc -n "supabase" | grep -E "supabase|kong|auth|rest|realtime|storage|meta|analytics|vector|imgproxy|functions|edge|pooler|db" || true
log "Federated services in namespace '$NAMESPACE':"
kubectl get svc -n "$NAMESPACE" | grep supabase-db || true
}
run_start() {
ensure_tools
local require_prole_db="0"
if needs_prole_db; then
require_prole_db="1"
fi
ensure_prereqs "$require_prole_db"
SUPABASE_HOME="$(detect_supabase_home)" || { err "ERROR: Supabase repo not found. Set SUPABASE_HOME or symlink ~/prole/supabase."; exit 1; }
log "Using Supabase repo: $SUPABASE_HOME"
log "Using namespace: $NAMESPACE"
case "$SUPABASE_BOOTSTRAP_MODE" in
clean)
log "Bootstrapping clean Supabase..."
convert_compose_to_k8s "$SUPABASE_HOME" clean
apply_k8s_resources clean
;;
prole)
log "Deploying Supabase with prole-db..."
convert_compose_to_k8s "$SUPABASE_HOME" prole
apply_k8s_resources prole
apply_db_migrations "$SUPABASE_HOME"
;;
clean+prole|clean-prole|clean_then_prole)
log "Bootstrapping clean Supabase..."
convert_compose_to_k8s "$SUPABASE_HOME" clean
apply_k8s_resources clean
log "Re-attaching prole-db as primary Supabase database..."
convert_compose_to_k8s "$SUPABASE_HOME" prole
apply_k8s_resources prole
apply_db_migrations "$SUPABASE_HOME"
;;
*)
err "ERROR: Unknown SUPABASE_BOOTSTRAP_MODE: $SUPABASE_BOOTSTRAP_MODE"
exit 1
;;
esac
}
case "$ACTION" in
start)
run_start
;;
stop)
ensure_tools
ensure_namespace
delete_k8s_resources
;;
restart)
delete_k8s_resources
run_start
;;
status)
status
;;
*)
usage
;;
esac

238
etc/init_supabase_ports.sh Executable file
View File

@ -0,0 +1,238 @@
#!/usr/bin/env bash
set -euo pipefail
# init_supabase_ports.sh
# Purpose:
# - Ensure the supabase namespace front-door (svc/db on port 5432)
# points to prole-db-rw in the provided namespace.
# - Ensure the Supabase reference Postgres service is on port 15432
# and not host-exposed.
PROG="init_supabase_ports"
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Load environment and config via prole_cfg.sh
# shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh"
SUPABASE_NAMESPACE="${SUPABASE_NAMESPACE:-supabase}"
PROLE_DB_SERVICE="${PROLE_DB_SERVICE:-prole-db-rw}"
SUPABASE_DB_SERVICE="${SUPABASE_DB_SERVICE:-db}"
SUPABASE_POSTGRES_SERVICE="${SUPABASE_POSTGRES_SERVICE:-supabase-postgres}"
SUPABASE_POSTGRES_PORT="${SUPABASE_POSTGRES_PORT:-15432}"
DB_PORT="${DB_PORT:-5432}"
RESTART_ON_CHANGE=1
usage() {
cat <<EOF
Usage:
$PROG -n <namespace> [options]
Required:
-n, --namespace <namespace> Namespace that contains svc/$PROLE_DB_SERVICE
Options:
--supabase-namespace <ns> Supabase namespace (default: $SUPABASE_NAMESPACE)
--no-restart Do not restart Supabase services on change
-h, --help Show this help
Notes:
- This script creates/updates svc/$SUPABASE_DB_SERVICE in the Supabase namespace
as an ExternalName pointing to $PROLE_DB_SERVICE.<namespace>.svc.cluster.local:5432.
- When the target namespace changes, Supabase services are restarted automatically.
EOF
}
log() { printf '%s\n' "$*"; }
warn() { printf '[warn] %s\n' "$*"; }
err() { printf '[error] %s\n' "$*" >&2; }
TARGET_NAMESPACE=""
while [[ $# -gt 0 ]]; do
case "$1" in
-n|--namespace)
TARGET_NAMESPACE="${2:-}"
shift 2
;;
--supabase-namespace)
SUPABASE_NAMESPACE="${2:-}"
shift 2
;;
--no-restart)
RESTART_ON_CHANGE=0
shift
;;
-h|--help)
usage
exit 0
;;
*)
err "Unknown argument: $1"
usage
exit 2
;;
esac
done
if [[ -z "$TARGET_NAMESPACE" ]]; then
err "-n|--namespace is required"
usage
exit 1
fi
ensure_tools() {
for t in kubectl; do
command -v "$t" >/dev/null || { err "Missing required tool: $t"; exit 1; }
done
}
ensure_namespace() {
if ! kubectl get namespace "$SUPABASE_NAMESPACE" >/dev/null 2>&1; then
log "Creating namespace '$SUPABASE_NAMESPACE' ..."
kubectl create namespace "$SUPABASE_NAMESPACE" >/dev/null 2>&1 || true
fi
}
selector_pairs_to_yaml() {
local selector="$1"
local out=""
local pair key val
IFS=',' read -ra pairs <<<"$selector"
for pair in "${pairs[@]}"; do
pair="$(printf '%s' "$pair" | xargs)"
[[ -z "$pair" ]] && continue
key="${pair%%=*}"
val="${pair#*=}"
[[ -z "$key" || -z "$val" ]] && continue
out+=" ${key}: ${val}\n"
done
printf '%b' "$out"
}
get_selector_pairs() {
kubectl -n "$SUPABASE_NAMESPACE" get svc "$1" \
-o jsonpath='{range $k,$v := .spec.selector}{$k}={$v},{end}' 2>/dev/null | sed 's/,$//'
}
restart_supabase() {
log "Restarting Supabase workloads in namespace '$SUPABASE_NAMESPACE' ..."
local restarted=0
if kubectl -n "$SUPABASE_NAMESPACE" get deploy >/dev/null 2>&1; then
if kubectl -n "$SUPABASE_NAMESPACE" rollout restart deploy --all >/dev/null 2>&1; then
restarted=1
fi
fi
if kubectl -n "$SUPABASE_NAMESPACE" get sts >/dev/null 2>&1; then
if kubectl -n "$SUPABASE_NAMESPACE" rollout restart statefulset --all >/dev/null 2>&1; then
restarted=1
fi
fi
if [[ "$restarted" -eq 0 ]]; then
warn "Rollout restart not supported; deleting Supabase pods instead."
kubectl -n "$SUPABASE_NAMESPACE" delete pod --all --ignore-not-found >/dev/null 2>&1 || true
fi
}
ensure_tools
ensure_namespace
if ! kubectl -n "$TARGET_NAMESPACE" get svc "$PROLE_DB_SERVICE" >/dev/null 2>&1; then
warn "Service not found: $PROLE_DB_SERVICE in namespace $TARGET_NAMESPACE"
fi
DESIRED_EXTERNAL="${PROLE_DB_SERVICE}.${TARGET_NAMESPACE}.svc.cluster.local"
service_exists=0
current_type=""
current_external=""
current_selector=""
if kubectl -n "$SUPABASE_NAMESPACE" get svc "$SUPABASE_DB_SERVICE" >/dev/null 2>&1; then
service_exists=1
current_type=$(kubectl -n "$SUPABASE_NAMESPACE" get svc "$SUPABASE_DB_SERVICE" -o jsonpath='{.spec.type}' 2>/dev/null || true)
current_external=$(kubectl -n "$SUPABASE_NAMESPACE" get svc "$SUPABASE_DB_SERVICE" -o jsonpath='{.spec.externalName}' 2>/dev/null || true)
current_selector=$(get_selector_pairs "$SUPABASE_DB_SERVICE")
fi
namespace_changed=1
if [[ "$service_exists" -eq 1 && "$current_type" == "ExternalName" && "$current_external" == "$DESIRED_EXTERNAL" ]]; then
namespace_changed=0
fi
# Preserve selectors from the previous db service for the reference Postgres service.
DB_SELECTOR="$current_selector"
if [[ -z "$DB_SELECTOR" ]]; then
DB_SELECTOR="${SUPABASE_DB_SELECTOR:-io.kompose.service=db}"
fi
if [[ "$service_exists" -eq 1 && "$current_type" != "ExternalName" ]]; then
log "Replacing service $SUPABASE_NAMESPACE/$SUPABASE_DB_SERVICE with ExternalName -> $DESIRED_EXTERNAL"
kubectl -n "$SUPABASE_NAMESPACE" delete svc "$SUPABASE_DB_SERVICE" --ignore-not-found >/dev/null 2>&1 || true
fi
log "Ensuring $SUPABASE_NAMESPACE/$SUPABASE_DB_SERVICE points to $DESIRED_EXTERNAL:$DB_PORT"
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: ${SUPABASE_DB_SERVICE}
namespace: ${SUPABASE_NAMESPACE}
annotations:
prole.org/db-namespace: "${TARGET_NAMESPACE}"
prole.org/db-service: "${PROLE_DB_SERVICE}"
spec:
type: ExternalName
externalName: ${DESIRED_EXTERNAL}
ports:
- name: postgres
port: ${DB_PORT}
targetPort: ${DB_PORT}
EOF
# Ensure Supabase reference Postgres service is on 15432 and ClusterIP
supabase_pg_exists=0
supabase_pg_type=""
if kubectl -n "$SUPABASE_NAMESPACE" get svc "$SUPABASE_POSTGRES_SERVICE" >/dev/null 2>&1; then
supabase_pg_exists=1
supabase_pg_type=$(kubectl -n "$SUPABASE_NAMESPACE" get svc "$SUPABASE_POSTGRES_SERVICE" -o jsonpath='{.spec.type}' 2>/dev/null || true)
fi
if [[ "$supabase_pg_exists" -eq 1 && "$supabase_pg_type" != "ClusterIP" ]]; then
log "Replacing service $SUPABASE_NAMESPACE/$SUPABASE_POSTGRES_SERVICE with ClusterIP on ${SUPABASE_POSTGRES_PORT}"
kubectl -n "$SUPABASE_NAMESPACE" delete svc "$SUPABASE_POSTGRES_SERVICE" --ignore-not-found >/dev/null 2>&1 || true
supabase_pg_exists=0
fi
selector_yaml="$(selector_pairs_to_yaml "$DB_SELECTOR")"
if [[ -z "$selector_yaml" ]]; then
warn "Unable to determine selector for $SUPABASE_POSTGRES_SERVICE; skipping creation/update."
else
log "Ensuring $SUPABASE_NAMESPACE/$SUPABASE_POSTGRES_SERVICE uses port ${SUPABASE_POSTGRES_PORT}"
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: ${SUPABASE_POSTGRES_SERVICE}
namespace: ${SUPABASE_NAMESPACE}
labels:
app: supabase-postgres
spec:
type: ClusterIP
selector:
${selector_yaml} ports:
- name: postgres
port: ${SUPABASE_POSTGRES_PORT}
targetPort: ${DB_PORT}
EOF
fi
if [[ "$namespace_changed" -eq 1 && "$RESTART_ON_CHANGE" -eq 1 ]]; then
restart_supabase
fi
log "Supabase port wiring complete."

1118
install.py

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,9 @@ import curses
import sys
import threading
import os
import subprocess
import getpass
import json
from pathlib import Path
from typing import Optional, Callable
@ -41,7 +43,19 @@ class ProleNcursesInstaller:
self.selected_local_path = None
# Environment/config variables
self.cluster_env = "development"
self.cluster_env = "k3d-prole-dev-cluster"
self.cluster_environments = ["prole-dev-cluster", "prole-service-cluster", "prole-prod-cluster"]
self.selected_env_index = 0
self.kubectx_list = self._get_kubectx_list()
self.selected_kubectx_index = 0
self.k3s_services_status = {"registry": "Unknown", "openbao": "Unknown"}
self.prod_artifacts_path = InputField(self.main_content_win, 15, 4, 50)
self.prod_artifacts_path.set_value(str(self.project_root / "data" / "staging"))
# Connection details for remote k3s
self.k3s_server = "https://pi.prole.org:6443"
self.k3s_token = "K10af6fadc27a4cc8b859a10fc69259bc90378c54ecb5f895cbe7c54c6954faa7e0::server:7ad0aa18511842387814d4fb4bf6461f"
try:
self.namespace_owner = getpass.getuser()
except Exception:
@ -115,8 +129,8 @@ class ProleNcursesInstaller:
("deps_summary", self._render_deps_summary),
("network_scan", self._render_network_scan),
("env_setup", self._render_env_setup),
("init_password", self._render_init_password),
("init_cluster", self._render_init_cluster),
("init_password", self._render_init_password),
("init_db_build", self._render_init_db_build),
("init_scripts", self._render_init_scripts),
("kerberos_config", self._render_kerberos_config),
@ -189,8 +203,8 @@ class ProleNcursesInstaller:
("Dependencies", "deps_summary"),
("Network Scan", "network_scan"),
("Environment", "env_setup"),
("Cluster Environment", "init_cluster"),
("Password", "init_password"),
("Cluster", "init_cluster"),
("Build", "init_db_build"),
("Scripts", "init_scripts"),
("Kerberos", "kerberos_config"),
@ -229,7 +243,12 @@ class ProleNcursesInstaller:
return
elif key == ord('q') or key == ord('Q'):
self.running = False
elif key == curses.KEY_LEFT or key == ord('h'):
# Delegate to page-specific handler first
if self._handle_page_input(key):
return
if key == curses.KEY_LEFT or key == ord('h'):
self.footer.move_selection(-1)
elif key == curses.KEY_RIGHT or key == ord('l'):
self.footer.move_selection(1)
@ -247,6 +266,106 @@ class ProleNcursesInstaller:
except curses.error:
pass
def _handle_page_input(self, key: int) -> bool:
"""Handle page-specific keyboard input. Returns True if handled."""
page_id = self.pages[self.page_index][0] if self.page_index < len(self.pages) else ""
if page_id == "init_cluster":
if key == curses.KEY_UP or key == ord('k'):
self.selected_env_index = (self.selected_env_index - 1) % len(self.cluster_environments)
self.cluster_env = self.cluster_environments[self.selected_env_index]
if self.cluster_env == "prole-service-cluster":
self._verify_k3s_services()
return True
elif key == curses.KEY_DOWN or key == ord('j'):
self.selected_env_index = (self.selected_env_index + 1) % len(self.cluster_environments)
self.cluster_env = self.cluster_environments[self.selected_env_index]
if self.cluster_env == "prole-service-cluster":
self._verify_k3s_services()
return True
selected_env = self.cluster_environments[self.selected_env_index]
if selected_env == "prole-dev-cluster":
if key == ord(' '): # Toggle/Select context
pass # We could implement sub-selection
elif key == curses.KEY_NPAGE: # Page Down to scroll context list?
self.selected_kubectx_index = (self.selected_kubectx_index + 1) % len(self.kubectx_list)
return True
elif key == curses.KEY_PPAGE: # Page Up to scroll context list?
self.selected_kubectx_index = (self.selected_kubectx_index - 1) % len(self.kubectx_list)
return True
elif selected_env == "prole-service-cluster":
if key == ord('d') or key == ord('D'):
self._deploy_k3s_services()
return True
elif selected_env == "prole-prod-cluster":
return self.prod_artifacts_path.handle_key(key)
return False
def _get_kubectx_list(self) -> list[str]:
"""Get list of kubernetes contexts."""
try:
res = subprocess.run(["kubectx"], capture_output=True, text=True)
if res.returncode == 0:
return res.stdout.strip().split('\n')
# Fallback to kubectl
res = subprocess.run(["kubectl", "config", "get-contexts", "-o", "name"], capture_output=True, text=True)
if res.returncode == 0:
return res.stdout.strip().split('\n')
except Exception:
pass
return ["default"]
def _verify_k3s_services(self):
"""Verify registry:2 and openbao on remote k3s cluster."""
def _verify():
self.k3s_services_status = {"registry": "Checking...", "openbao": "Checking..."}
base_cmd = [
"kubectl",
"--server=" + self.k3s_server,
"--token=" + self.k3s_token,
"--insecure-skip-tls-verify=true"
]
try:
# Check registry
res = subprocess.run(base_cmd + ["get", "service", "-A"], capture_output=True, text=True)
if "registry" in res.stdout.lower():
self.k3s_services_status["registry"] = "Good"
else:
self.k3s_services_status["registry"] = "Failing"
# In a real scenario, we might trigger deployment here
# Check openbao
if "openbao" in res.stdout.lower() or "bao" in res.stdout.lower():
self.k3s_services_status["openbao"] = "Good"
else:
self.k3s_services_status["openbao"] = "Failing"
except Exception as e:
self.status_message = f"K3s connection error: {str(e)}"
self.k3s_services_status = {"registry": "Error", "openbao": "Error"}
threading.Thread(target=_verify).start()
def _deploy_k3s_services(self):
"""Deploy registry and openbao to remote k3s cluster."""
def _deploy():
self.status_message = "Deploying services to remote k3s..."
# In a real scenario, we'd run a script or apply manifests
# base_cmd = [...]
# subprocess.run(base_cmd + ["apply", "-f", ...])
# Simulate deployment delay
import time
time.sleep(2)
self._verify_k3s_services()
self.status_message = "Deployment triggered on remote k3s."
threading.Thread(target=_deploy).start()
def _update_footer(self):
"""Update footer buttons based on current page."""
page_id = self.pages[self.page_index][0] if self.page_index < len(self.pages) else ""
@ -393,12 +512,49 @@ class ProleNcursesInstaller:
def _render_init_cluster(self):
"""Render cluster initialization screen."""
win = CursesWindow(self.main_content_win)
win.render_title("Initialize Database Cluster", y=2)
win.render_title("Cluster Environment", y=2)
win.render_paragraph(
"Initializing PostgreSQL cluster and configuration...",
"Select the target cluster environment and verify k3d/k3s/prod plus OpenBao are available.",
y=5
)
y = 9
for i, env in enumerate(self.cluster_environments):
selector = "(*)" if i == self.selected_env_index else "( )"
win.render_text(y, 4, f"{selector} {env}")
y += 1
y += 1
selected_env = self.cluster_environments[self.selected_env_index]
if selected_env == "prole-dev-cluster":
win.render_text(y, 4, "Available kubectx contexts (Page Up/Down to scroll):", curses.A_BOLD)
y += 1
start_idx = max(0, self.selected_kubectx_index - 2)
for i, ctx in enumerate(self.kubectx_list[start_idx:start_idx+5]):
attr = curses.A_REVERSE if (start_idx + i) == self.selected_kubectx_index else curses.A_NORMAL
win.render_text(y, 6, f"- {ctx}", attr)
y += 1
elif selected_env == "prole-service-cluster":
win.render_text(y, 4, "Remote K3s Service Status:", curses.A_BOLD)
y += 1
for service, status in self.k3s_services_status.items():
color = curses.color_pair(2) if status == "Good" else curses.color_pair(3) if status == "Failing" else curses.A_NORMAL
win.render_text(y, 6, f"{service}: ")
win.render_text(y, 6 + len(service) + 2, status, color)
y += 1
if any(s == "Failing" for s in self.k3s_services_status.values()):
win.render_text(y + 1, 4, "Press 'd' to deploy missing services", curses.A_BOLD)
elif selected_env == "prole-prod-cluster":
win.render_text(y, 4, "Local Artifact Staging Directory:", curses.A_BOLD)
y += 2
self.prod_artifacts_path.render()
win.render_text(y + 2, 4, "(Used by etc/deploy_pipeline.sh --mode gcp)")
def _render_init_scripts(self):
"""Render initialization scripts screen."""
win = CursesWindow(self.main_content_win)

View File

@ -145,7 +145,7 @@ def canvas_line_on(cnv: tk.Canvas, x1: int, y1: int, x2: int, y2: int, *, fill:
def create_nav_footer(parent, buttons: list[tuple[int, str]], commands: dict[int, callable] | None = None,
style_name: str = 'Nav.TButton') -> dict[int, tk.Button]:
style_name: str = 'Nav.TButton', gear_command: callable | None = None) -> dict[int, tk.Button]:
"""Create a right-aligned navigation footer with uniform button styling.
Uses tk.Button instead of ttk.Button for better color control on macOS.
@ -158,12 +158,29 @@ def create_nav_footer(parent, buttons: list[tuple[int, str]], commands: dict[int
divider = tk.Frame(footer, bg='#CCCCCC', height=1)
divider.pack(side='top', fill='x')
btn_map: dict[int | str, tk.Button] = {}
if gear_command:
gear_btn = tk.Button(footer,
text="⚙️",
command=gear_command,
bg='#F5F5DC',
fg='black',
activebackground='#E5E5D5',
activeforeground='black',
highlightbackground='#F5F5DC',
highlightthickness=0,
relief='flat',
font=('SF Pro Text', 18),
padx=10,
pady=8)
gear_btn.pack(side='left', padx=(20, 0), pady=12)
btn_map['gear'] = gear_btn
# Flexible spacer to push buttons to the right
spacer = tk.Frame(footer, bg='#F5F5DC')
spacer.pack(side='left', expand=True, fill='x')
cmds = commands or {}
btn_map: dict[int, tk.Button] = {}
for btn_id, title in buttons:
cmd = cmds.get(btn_id)
# Use tk.Button for full control over background and borders on macOS

View File

@ -12,9 +12,6 @@ resources:
- prole-service.yaml
- openbao-statefulset.yaml
- openbao-service.yaml
- supabase-configmap.yaml
- supabase-deployment.yaml
- supabase-service.yaml
- prometheus-configmap.yaml
- prometheus-deployment.yaml
- prometheus-service.yaml

View File

@ -1,10 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: supabase-config
labels:
app: supabase
data:
POSTGRES_HOST: prole-db-postgres
POSTGRES_PORT: "5432"
POSTGRES_DB: prole-db

View File

@ -1,49 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: supabase
labels:
app: supabase
spec:
replicas: 1
selector:
matchLabels:
app: supabase
template:
metadata:
labels:
app: supabase
spec:
containers:
- name: supabase
image: supabase/supabase:latest
imagePullPolicy: IfNotPresent
env:
- name: POSTGRES_HOST
valueFrom:
configMapKeyRef:
name: supabase-config
key: POSTGRES_HOST
- name: POSTGRES_PORT
valueFrom:
configMapKeyRef:
name: supabase-config
key: POSTGRES_PORT
- name: POSTGRES_DB
valueFrom:
configMapKeyRef:
name: supabase-config
key: POSTGRES_DB
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: prole-db-user
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: prole-db-user
key: password
ports:
- name: https
containerPort: 443

View File

@ -1,14 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: supabase
labels:
app: supabase
spec:
selector:
app: supabase
ports:
- name: https
port: 443
targetPort: https
type: ClusterIP

View File

@ -2,6 +2,11 @@
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
DEV_HOME="${DEV_HOME:-$HOME/dev}"
DEV_HOME="${DEV_HOME/#\~/$HOME}"
SUPABASE_DIR="$DEV_HOME/supabase"
@ -10,10 +15,18 @@ 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}"
DOCKER_IMPORT_DIR="${DOCKER_IMPORT_DIR:-$PROJECT_ROOT/data/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-postgres}"
PROLE_DB_NAMESPACE="${PROLE_DB_NAMESPACE:-}"
MODE=""
MODE="k3d"
USE_DEV_HELPERS="false"
FOREGROUND="false"
FORCE="true"
usage() {
cat <<'USAGE'
@ -24,17 +37,21 @@ README/DEVELOPERS guidance (Docker Compose). This is a multi-service deployment
(Postgres, Auth, Storage, Realtime, Kong, Studio, etc.)
Usage:
./deploy.sh --mode <local|k3d> [options]
./deploy.sh [options]
Options:
--mode <local|k3d> Deployment mode ('local' for Docker Compose, 'k3d' for Kubernetes)
--mode <local|k3d> Deployment mode ('local' for Docker Compose, 'k3d' for Kubernetes; default: k3d)
--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)
-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)
@ -67,6 +84,16 @@ detect_platform() {
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"
@ -79,6 +106,75 @@ compose_cmd() {
die "Docker Compose not found (expected 'docker compose' or 'docker-compose')"
}
ensure_k3d() {
require_cmd k3d
}
ensure_kompose() {
require_cmd kompose
}
list_k3d_clusters() {
if k3d cluster list -o json >/dev/null 2>&1; then
k3d cluster list -o json | python - <<'PY'
import json
import sys
data = json.load(sys.stdin)
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
return 0
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
@ -94,6 +190,14 @@ parse_args() {
FOREGROUND="true"
shift
;;
-f|--force)
FORCE="true"
shift
;;
--no-force)
FORCE="false"
shift
;;
-h|--help)
usage
exit 0
@ -115,6 +219,15 @@ ensure_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"
@ -166,6 +279,99 @@ run_local() {
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
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"
mkdir -p "$DOCKER_IMPORT_DIR"
log "Saving $img ($platform) to $tar_path"
docker save -o "$tar_path" "$platform_tag" >/dev/null
}
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" >/dev/null
docker tag "$platform_tag" "$img"
}
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"
@ -175,11 +381,8 @@ prefetch_k3d_images() {
ensure_env_file
local 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
local files
files=($(compose_files))
local images
images=$(cd "$DOCKER_DIR" && $compose "${files[@]}" --env-file ".env" config --images | awk 'NF' | sort -u)
@ -187,33 +390,168 @@ prefetch_k3d_images() {
die "No images found in Supabase compose config"
fi
log "Prefetching Supabase images for platform '$SUPABASE_IMAGE_PLATFORM'..."
log "Supabase images discovered:"
printf " - %s\n" $images
for img in $images; do
if docker pull --platform "$SUPABASE_IMAGE_PLATFORM" "$img" >/dev/null 2>&1; then
continue
fi
write_artifact_manifest "$images"
log "Image artifact list written to $DOCKER_IMPORT_DIR/supabase-images.txt"
if docker image inspect "$img" >/dev/null 2>&1; then
warn "Using local image for $img (pull failed)"
continue
fi
if [[ "$USE_DEV_HELPERS" == "true" ]]; then
warn "Pull failed for $img; attempting docker compose build for dev helpers..."
(cd "$DOCKER_DIR" && $compose "${files[@]}" --env-file ".env" build) || true
if docker image inspect "$img" >/dev/null 2>&1; then
continue
fi
fi
die "Failed to pull or build image: $img"
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 archive="${SUPABASE_IMAGE_ARCHIVE:-$SCRIPT_DIR/../build/supabase-images-${SUPABASE_IMAGE_PLATFORM//\//-}.tar}"
mkdir -p "$(dirname "$archive")"
log "Saving Supabase images to $archive"
docker save -o "$archive" $images >/dev/null
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
log "Importing images into k3d"
for img in $images; do
if [[ -n "${K3D_CLUSTER_NAME:-}" ]]; then
k3d image import "$img" -c "$K3D_CLUSTER_NAME"
else
k3d image import "$img"
fi
done
}
resolve_prole_db_namespace() {
if [[ -n "${PROLE_DB_NAMESPACE:-}" ]]; then
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" <<EOF
apiVersion: v1
kind: Service
metadata:
name: supabase-postgres
namespace: supabase
labels:
app: supabase-postgres
spec:
selector:
io.kompose.service: db
ports:
- name: postgres
port: ${SUPABASE_POSTGRES_PORT}
targetPort: 5432
type: ClusterIP
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: ${PROLE_DB_SERVICE}.${PROLE_DB_NAMESPACE}.svc.cluster.local
ports:
- name: postgres
port: 5432
targetPort: 5432
EOF
}
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)
resolve_prole_db_namespace
if ! kubectl get svc "$PROLE_DB_SERVICE" -n "$PROLE_DB_NAMESPACE" >/dev/null 2>&1; then
warn "Prole DB service not found: ${PROLE_DB_SERVICE} in namespace ${PROLE_DB_NAMESPACE}"
fi
patch_db_deployment_port
write_supabase_postgres_service
write_db_alias_service
}
ensure_supabase_namespace() {
if ! kubectl get namespace supabase >/dev/null 2>&1; then
log "Creating 'supabase' namespace..."
kubectl create namespace supabase
fi
}
force_reset_supabase_namespace() {
log "Resetting 'supabase' namespace..."
kubectl delete namespace supabase --ignore-not-found >/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
raw = subprocess.check_output(["kubectl", "get", "namespace", "supabase", "-o", "json"])
data = json.loads(raw)
data["spec"]["finalizers"] = []
print(json.dumps(data))
PY
kubectl wait --for=delete namespace/supabase --timeout=120s >/dev/null 2>&1 || true
fi
fi
ensure_supabase_namespace
}
run_k3d() {
@ -223,30 +561,32 @@ run_k3d() {
log "Repo: $SUPABASE_DIR"
require_cmd kubectl
ensure_k3d
ensure_k3d_cluster
kubectl cluster-info >/dev/null 2>&1 || die "Kubernetes cluster not reachable"
# Ensure the 'supabase' namespace exists
if ! kubectl get namespace supabase >/dev/null 2>&1; then
log "Creating 'supabase' namespace..."
kubectl create namespace supabase
if [[ "$FORCE" == "true" ]]; then
force_reset_supabase_namespace
else
ensure_supabase_namespace
fi
# Call init_supabase.sh start
local init_script="$SCRIPT_DIR/../etc/init_supabase.sh"
[[ -f "$init_script" ]] || die "Missing init script: $init_script"
log "Running $init_script start..."
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:-$(detect_platform)}"
prefetch_k3d_images
export SUPABASE_BOOTSTRAP_MODE="${SUPABASE_BOOTSTRAP_MODE:-clean+prole}"
export SUPABASE_STAGE_IMAGES="${SUPABASE_STAGE_IMAGES:-1}"
export SUPABASE_IMAGE_STAGE_METHOD="${SUPABASE_IMAGE_STAGE_METHOD:-registry}"
export SUPABASE_IMAGE_PLATFORM
SUPABASE_IMAGE_PLATFORM="$(normalize_platform "${SUPABASE_IMAGE_PLATFORM:-}")"
bash "$init_script" start
prefetch_k3d_images
generate_k8s_manifests
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"
log "Deployment complete."
log "Access Studio via Kong proxy (check ingress/service in 'supabase' namespace)."

View File

@ -70,10 +70,11 @@ def test_navigation_flow_standard(mock_installer):
assert mock_installer.pages[mock_installer.page_index][0] == 'env_setup'
mock_installer.on_next()
assert mock_installer.pages[mock_installer.page_index][0] == 'kerberos_config'
assert mock_installer.pages[mock_installer.page_index][0] == 'init_cluster'
mock_installer.on_next()
assert mock_installer.pages[mock_installer.page_index][0] == 'init_password'
with patch.object(mock_installer, '_cluster_ready_for_navigation', return_value=True):
mock_installer.on_next()
assert mock_installer.pages[mock_installer.page_index][0] == 'init_password'
def test_navigation_flow_missing_deps(mock_installer):
# Welcome -> Dependencies