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

View File

@ -49,6 +49,20 @@ ensure_tools() {
done 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() { ensure_namespace() {
if ! kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then if ! kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then
log "Creating namespace '$NAMESPACE' ..." log "Creating namespace '$NAMESPACE' ..."
@ -227,6 +241,54 @@ patch_cnpg_cluster_for_auth() {
fi 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() { apply_krb5_conf_to_cnpg_pods() {
local conf local conf
conf=$(get_krb5_conf) conf=$(get_krb5_conf)
@ -362,8 +424,12 @@ initialize() {
ensure_krb5_conf_configmap ensure_krb5_conf_configmap
patch_cnpg_cluster_for_auth || true patch_cnpg_cluster_for_auth || true
wait_for_cnpg_pods "$CNPG_WAIT_TIMEOUT" if apply_krb5_conf_mount_to_cnpg; then
apply_krb5_conf_to_cnpg_pods 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." log "Kerberos initialization complete."
} }

View File

@ -32,7 +32,9 @@ KRB5_REALM=${KRB5_REALM:-${REALM:-}}
KRB5_KDC=${KRB5_KDC:-} KRB5_KDC=${KRB5_KDC:-}
KRB5_USER=${KRB5_USER:-${KRB5_USERNAME:-}} KRB5_USER=${KRB5_USER:-${KRB5_USERNAME:-}}
KRB5_PASSWORD=${KRB5_PASSWORD:-} 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} KEEP_POD=${KEEP_POD:-0}
KRB5_TEST_HOST_NETWORK=${KRB5_TEST_HOST_NETWORK:-0} KRB5_TEST_HOST_NETWORK=${KRB5_TEST_HOST_NETWORK:-0}
KRB5_TEST_DNS_POLICY=${KRB5_TEST_DNS_POLICY:-} KRB5_TEST_DNS_POLICY=${KRB5_TEST_DNS_POLICY:-}
@ -55,40 +57,7 @@ detect_image() {
echo "$KRB5_TEST_IMAGE" echo "$KRB5_TEST_IMAGE"
return return
fi fi
local pg_version_file release_file pg_version release echo "ubuntu:24.04"
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}"
} }
ensure_configmap() { ensure_configmap() {
@ -124,20 +93,102 @@ ${host_net_block} restartPolicy: Never
- name: kerberos-test - name: kerberos-test
image: ${image} image: ${image}
command: ["sleep","3600"] 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 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() { run_test() {
ensure_tools ensure_tools
ensure_namespace ensure_namespace
@ -173,9 +224,20 @@ run_test() {
exit 1 exit 1
fi 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..." echo "Checking for kinit in pod..."
if ! kubectl -n "$NAMESPACE" exec "$pod_name" -- sh -c 'command -v kinit >/dev/null 2>&1'; then 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 if [[ "$KEEP_POD" != "1" ]]; then
kubectl -n "$NAMESPACE" delete pod "$pod_name" --ignore-not-found kubectl -n "$NAMESPACE" delete pod "$pod_name" --ignore-not-found
fi fi
@ -195,11 +257,12 @@ run_test() {
kubectl -n "$NAMESPACE" exec "$pod_name" -- klist || true kubectl -n "$NAMESPACE" exec "$pod_name" -- klist || true
if [[ "${REALM_JOIN:-0}" == "1" ]]; then if [[ "${REALM_JOIN:-0}" == "1" ]]; then
echo "Attempting realm join inside test pod..." if ! join_realm "$pod_name"; then
if kubectl -n "$NAMESPACE" exec "$pod_name" -- sh -c 'command -v realm >/dev/null 2>&1'; then echo "ERROR: realm join failed." >&2
printf '%s\n' "$KRB5_PASSWORD" | kubectl -n "$NAMESPACE" exec -i "$pod_name" -- realm join --user "$KRB5_USER" "$KRB5_REALM" || true if [[ "$KEEP_POD" != "1" ]]; then
else kubectl -n "$NAMESPACE" delete pod "$pod_name" --ignore-not-found
echo "realm command not found in image; skipping realm join." fi
exit 1
fi fi
fi fi

View File

@ -18,11 +18,12 @@ CONFIG_FILE="$PROLE_HOME/conf/port-mappings.properties"
usage() { usage() {
cat <<EOF cat <<EOF
Usage: 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: Options:
-c, --config-file=FILE Path to local-ports.properties (XML) -c, --config-file=FILE Path to local-ports.properties (XML)
-v, --verbose Verbose output -v, --verbose Verbose output
-f, --force Force: kill existing processes blocking ports
Examples: Examples:
$PROG -c ./port-mappings.properties start $PROG -c ./port-mappings.properties start
@ -32,6 +33,7 @@ EOF
} }
TARGET_ID="" TARGET_ID=""
FORCE=0
log() { printf '%s\n' "$*"; } log() { printf '%s\n' "$*"; }
vlog() { [ "$VERBOSE" -eq 1 ] && printf '[verbose] %s\n' "$*" || true; } 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" PF_MANAGEMENT_NAMESPACE="$NAMESPACE"
fi fi
SUPABASE_NAMESPACE="${SUPABASE_NAMESPACE:-supabase}" # SUPABASE_NAMESPACE="${SUPABASE_NAMESPACE:-supabase}"
SUPABASE_DB_TARGET="${SUPABASE_DB_TARGET:-${SUPABASE_DB_SERVICE:-svc/db}}" # 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 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_BASE="${PROLE_DB_ALT_PORT_BASE:-15432}"
PROLE_DB_ALT_PORT_EFFECTIVE="" PROLE_DB_ALT_PORT_EFFECTIVE=""
@ -415,43 +412,16 @@ foreach_mapping() {
ns="${ns//\$\{PROLE_MANAGEMENT_NAMESPACE\}/$PF_MANAGEMENT_NAMESPACE}" ns="${ns//\$\{PROLE_MANAGEMENT_NAMESPACE\}/$PF_MANAGEMENT_NAMESPACE}"
fi fi
# Mapping-specific overrides # Mapping-specific overrides (removed hardcoded namespace 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
if [ "$id" = "postgres" ]; then if [ "$id" = "postgres" ]; then
if [ -n "${DB_HOST_PORT:-}" ]; then if [ -n "${DB_HOST_PORT:-}" ]; then
hostPort="$DB_HOST_PORT" hostPort="$DB_HOST_PORT"
fi fi
if [ "$SUPABASE_ENABLED_EFFECTIVE" -eq 1 ]; then # Supabase override removed
ns="$SUPABASE_NAMESPACE"
target="$SUPABASE_DB_TARGET"
fi
fi fi
if [ "$id" = "prole-db" ]; then # prole-db logic removed, now uses XML
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
# Basic validation # Basic validation
if [ -z "$id" ] || [ -z "$ns" ] || [ -z "$target" ] || [ -z "$hostPort" ] || [ -z "$servicePort" ]; then if [ -z "$id" ] || [ -z "$ns" ] || [ -z "$target" ] || [ -z "$hostPort" ] || [ -z "$servicePort" ]; then
@ -584,11 +554,61 @@ status_one() {
fi 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() { do_start() {
validate_env validate_env
# Preflight: require Docker daemon and k3d (if applicable) # Preflight: require Docker daemon and k3d (if applicable)
ensure_docker_running ensure_docker_running
ensure_k3d_ready_if_applicable ensure_k3d_ready_if_applicable
vlog "Scanning for port collisions..."
foreach_mapping scan_for_collisions
foreach_mapping start_port_forward foreach_mapping start_port_forward
} }
@ -603,7 +623,11 @@ do_restart() {
# Preflight: require Docker daemon and k3d (if applicable) # Preflight: require Docker daemon and k3d (if applicable)
ensure_docker_running ensure_docker_running
ensure_k3d_ready_if_applicable 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 stop_port_forward
foreach_mapping scan_for_collisions
foreach_mapping start_port_forward foreach_mapping start_port_forward
} }
@ -682,6 +706,10 @@ while [ $# -gt 0 ]; do
VERBOSE=1 VERBOSE=1
shift shift
;; ;;
-f|--force)
FORCE=1
shift
;;
-c) -c)
shift shift
[ $# -gt 0 ] || { err "-c requires a file path"; usage; exit 2; } [ $# -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."

1150
install.py

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,9 @@ import curses
import sys import sys
import threading import threading
import os import os
import subprocess
import getpass import getpass
import json
from pathlib import Path from pathlib import Path
from typing import Optional, Callable from typing import Optional, Callable
@ -41,7 +43,19 @@ class ProleNcursesInstaller:
self.selected_local_path = None self.selected_local_path = None
# Environment/config variables # 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: try:
self.namespace_owner = getpass.getuser() self.namespace_owner = getpass.getuser()
except Exception: except Exception:
@ -115,8 +129,8 @@ class ProleNcursesInstaller:
("deps_summary", self._render_deps_summary), ("deps_summary", self._render_deps_summary),
("network_scan", self._render_network_scan), ("network_scan", self._render_network_scan),
("env_setup", self._render_env_setup), ("env_setup", self._render_env_setup),
("init_password", self._render_init_password),
("init_cluster", self._render_init_cluster), ("init_cluster", self._render_init_cluster),
("init_password", self._render_init_password),
("init_db_build", self._render_init_db_build), ("init_db_build", self._render_init_db_build),
("init_scripts", self._render_init_scripts), ("init_scripts", self._render_init_scripts),
("kerberos_config", self._render_kerberos_config), ("kerberos_config", self._render_kerberos_config),
@ -189,8 +203,8 @@ class ProleNcursesInstaller:
("Dependencies", "deps_summary"), ("Dependencies", "deps_summary"),
("Network Scan", "network_scan"), ("Network Scan", "network_scan"),
("Environment", "env_setup"), ("Environment", "env_setup"),
("Cluster Environment", "init_cluster"),
("Password", "init_password"), ("Password", "init_password"),
("Cluster", "init_cluster"),
("Build", "init_db_build"), ("Build", "init_db_build"),
("Scripts", "init_scripts"), ("Scripts", "init_scripts"),
("Kerberos", "kerberos_config"), ("Kerberos", "kerberos_config"),
@ -229,7 +243,12 @@ class ProleNcursesInstaller:
return return
elif key == ord('q') or key == ord('Q'): elif key == ord('q') or key == ord('Q'):
self.running = False 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) self.footer.move_selection(-1)
elif key == curses.KEY_RIGHT or key == ord('l'): elif key == curses.KEY_RIGHT or key == ord('l'):
self.footer.move_selection(1) self.footer.move_selection(1)
@ -247,6 +266,106 @@ class ProleNcursesInstaller:
except curses.error: except curses.error:
pass 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): def _update_footer(self):
"""Update footer buttons based on current page.""" """Update footer buttons based on current page."""
page_id = self.pages[self.page_index][0] if self.page_index < len(self.pages) else "" 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): def _render_init_cluster(self):
"""Render cluster initialization screen.""" """Render cluster initialization screen."""
win = CursesWindow(self.main_content_win) win = CursesWindow(self.main_content_win)
win.render_title("Initialize Database Cluster", y=2) win.render_title("Cluster Environment", y=2)
win.render_paragraph( 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=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): def _render_init_scripts(self):
"""Render initialization scripts screen.""" """Render initialization scripts screen."""
win = CursesWindow(self.main_content_win) 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, 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. """Create a right-aligned navigation footer with uniform button styling.
Uses tk.Button instead of ttk.Button for better color control on macOS. 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 = tk.Frame(footer, bg='#CCCCCC', height=1)
divider.pack(side='top', fill='x') 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 # Flexible spacer to push buttons to the right
spacer = tk.Frame(footer, bg='#F5F5DC') spacer = tk.Frame(footer, bg='#F5F5DC')
spacer.pack(side='left', expand=True, fill='x') spacer.pack(side='left', expand=True, fill='x')
cmds = commands or {} cmds = commands or {}
btn_map: dict[int, tk.Button] = {}
for btn_id, title in buttons: for btn_id, title in buttons:
cmd = cmds.get(btn_id) cmd = cmds.get(btn_id)
# Use tk.Button for full control over background and borders on macOS # Use tk.Button for full control over background and borders on macOS

View File

@ -12,9 +12,6 @@ resources:
- prole-service.yaml - prole-service.yaml
- openbao-statefulset.yaml - openbao-statefulset.yaml
- openbao-service.yaml - openbao-service.yaml
- supabase-configmap.yaml
- supabase-deployment.yaml
- supabase-service.yaml
- prometheus-configmap.yaml - prometheus-configmap.yaml
- prometheus-deployment.yaml - prometheus-deployment.yaml
- prometheus-service.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 set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 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/dev}"
DEV_HOME="${DEV_HOME/#\~/$HOME}" DEV_HOME="${DEV_HOME/#\~/$HOME}"
SUPABASE_DIR="$DEV_HOME/supabase" 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" DEV_COMPOSE_FILE="$DOCKER_DIR/dev/docker-compose.dev.yml"
ENV_EXAMPLE="$DOCKER_DIR/.env.example" ENV_EXAMPLE="$DOCKER_DIR/.env.example"
ENV_FILE="$DOCKER_DIR/.env" 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" USE_DEV_HELPERS="false"
FOREGROUND="false" FOREGROUND="false"
FORCE="true"
usage() { usage() {
cat <<'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.) (Postgres, Auth, Storage, Realtime, Kong, Studio, etc.)
Usage: Usage:
./deploy.sh --mode <local|k3d> [options] ./deploy.sh [options]
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 --with-dev-helpers Include docker/dev/docker-compose.dev.yml
--foreground Run docker compose in the foreground (default: detached, local mode only) --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 -h, --help Show help
Notes: Notes:
- This script syncs the Supabase repo into $DEV_HOME/supabase. - This script syncs the Supabase repo into $DEV_HOME/supabase.
- The deployment runs as a single "supabase" namespace via the Compose project name. - 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: - This script preserves the README/DEVELOPERS steps:
1) use docker/docker-compose.yml 1) use docker/docker-compose.yml
2) copy docker/.env.example -> docker/.env (if missing) 2) copy docker/.env.example -> docker/.env (if missing)
@ -67,6 +84,16 @@ detect_platform() {
esac 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() { compose_cmd() {
if docker compose version >/dev/null 2>&1; then if docker compose version >/dev/null 2>&1; then
echo "docker compose" echo "docker compose"
@ -79,6 +106,75 @@ compose_cmd() {
die "Docker Compose not found (expected 'docker compose' or 'docker-compose')" 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() { parse_args() {
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
@ -94,6 +190,14 @@ parse_args() {
FOREGROUND="true" FOREGROUND="true"
shift shift
;; ;;
-f|--force)
FORCE="true"
shift
;;
--no-force)
FORCE="false"
shift
;;
-h|--help) -h|--help)
usage usage
exit 0 exit 0
@ -115,6 +219,15 @@ ensure_env_file() {
warn "docker/.env contains default secrets. Update them before any production use." 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() { ensure_repo() {
require_cmd git require_cmd git
mkdir -p "$DEV_HOME" mkdir -p "$DEV_HOME"
@ -166,6 +279,99 @@ run_local() {
log "Access Studio at http://localhost:8082 (see docker/.env for ports)." 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() { prefetch_k3d_images() {
require_cmd docker require_cmd docker
docker info >/dev/null 2>&1 || die "Docker daemon is not running" docker info >/dev/null 2>&1 || die "Docker daemon is not running"
@ -175,11 +381,8 @@ prefetch_k3d_images() {
ensure_env_file ensure_env_file
local files=("-f" "docker-compose.yml") local files
if [[ "$USE_DEV_HELPERS" == "true" ]]; then files=($(compose_files))
[[ -f "$DEV_COMPOSE_FILE" ]] || die "Missing dev helpers compose file: $DEV_COMPOSE_FILE"
files+=("-f" "dev/docker-compose.dev.yml")
fi
local images local images
images=$(cd "$DOCKER_DIR" && $compose "${files[@]}" --env-file ".env" config --images | awk 'NF' | sort -u) 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" die "No images found in Supabase compose config"
fi fi
log "Prefetching Supabase images for platform '$SUPABASE_IMAGE_PLATFORM'..." log "Supabase images discovered:"
printf " - %s\n" $images
for img in $images; do write_artifact_manifest "$images"
if docker pull --platform "$SUPABASE_IMAGE_PLATFORM" "$img" >/dev/null 2>&1; then log "Image artifact list written to $DOCKER_IMPORT_DIR/supabase-images.txt"
continue
fi
if docker image inspect "$img" >/dev/null 2>&1; then local platform
warn "Using local image for $img (pull failed)" for platform in $SUPABASE_IMAGE_PLATFORMS; do
continue log "Ensuring artifacts for platform '$platform' in $DOCKER_IMPORT_DIR"
fi for img in $images; do
ensure_image_artifact "$img" "$platform"
if [[ "$USE_DEV_HELPERS" == "true" ]]; then done
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"
done done
local archive="${SUPABASE_IMAGE_ARCHIVE:-$SCRIPT_DIR/../build/supabase-images-${SUPABASE_IMAGE_PLATFORM//\//-}.tar}" local deploy_platform
mkdir -p "$(dirname "$archive")" deploy_platform="$(normalize_platform "${SUPABASE_IMAGE_PLATFORM:-}")"
log "Saving Supabase images to $archive" log "Loading images for platform '$deploy_platform'"
docker save -o "$archive" $images >/dev/null 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() { run_k3d() {
@ -223,30 +561,32 @@ run_k3d() {
log "Repo: $SUPABASE_DIR" log "Repo: $SUPABASE_DIR"
require_cmd kubectl require_cmd kubectl
ensure_k3d
ensure_k3d_cluster
kubectl cluster-info >/dev/null 2>&1 || die "Kubernetes cluster not reachable" kubectl cluster-info >/dev/null 2>&1 || die "Kubernetes cluster not reachable"
# Ensure the 'supabase' namespace exists if [[ "$FORCE" == "true" ]]; then
if ! kubectl get namespace supabase >/dev/null 2>&1; then force_reset_supabase_namespace
log "Creating 'supabase' namespace..." else
kubectl create namespace supabase ensure_supabase_namespace
fi 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" export SUPABASE_HOME="$SUPABASE_DIR"
if [[ "$USE_DEV_HELPERS" == "true" ]]; then if [[ "$USE_DEV_HELPERS" == "true" ]]; then
export SUPABASE_USE_DEV_COMPOSE=1 export SUPABASE_USE_DEV_COMPOSE=1
fi fi
export SUPABASE_IMAGE_PLATFORM="${SUPABASE_IMAGE_PLATFORM:-$(detect_platform)}" export SUPABASE_IMAGE_PLATFORM
prefetch_k3d_images SUPABASE_IMAGE_PLATFORM="$(normalize_platform "${SUPABASE_IMAGE_PLATFORM:-}")"
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}"
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 "Deployment complete."
log "Access Studio via Kong proxy (check ingress/service in 'supabase' namespace)." 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' assert mock_installer.pages[mock_installer.page_index][0] == 'env_setup'
mock_installer.on_next() 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() with patch.object(mock_installer, '_cluster_ready_for_navigation', return_value=True):
assert mock_installer.pages[mock_installer.page_index][0] == 'init_password' mock_installer.on_next()
assert mock_installer.pages[mock_installer.page_index][0] == 'init_password'
def test_navigation_flow_missing_deps(mock_installer): def test_navigation_flow_missing_deps(mock_installer):
# Welcome -> Dependencies # Welcome -> Dependencies