refactor: rename backup script to etc/init_cnpg_backup.sh and optimize Supabase image prefetch

- Renamed etc/init_prole-db-backup.sh to etc/init_cnpg_backup.sh and updated all references.

- Enhanced CNPG backup script with authoritative status validation.

- Implemented SupabaseImagePreloadMilestone to front-load heavy image transfers.

- Updated supabase/deploy.sh with --prefetch-images-only and --skip-prefetch.

- Updated unit tests for CNPG backups and Supabase silent milestone sequencing.

- Included incidental environment updates from installer execution.

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
chrisfu 2026-03-02 00:33:52 -08:00
parent 5618b662dd
commit 6dc3560dcf
34 changed files with 1894 additions and 387 deletions

View File

@ -2,6 +2,10 @@
# Format: key: local=... remote=... ns=... svc=... address=...
argocd: local=8081 remote=80 ns=argocd svc=argocd-server address=0.0.0.0
supabase-studio: local=18080 remote=3000 ns=supabase svc=studio address=0.0.0.0
supabase-auth: local=9999 remote=9999 ns=supabase svc=auth address=127.0.0.1
supabase-rest: local=3001 remote=3000 ns=supabase svc=rest address=0.0.0.0
supabase-realtime: local=4000 remote=4000 ns=supabase svc=realtime address=0.0.0.0
garage: local=3900 remote=3900 ns=default svc=garage address=0.0.0.0
openbao: local=8200 remote=8200 ns=default svc=openbao address=127.0.0.1
opentofu: local=8080 remote=8080 ns=default svc=opentofu address=0.0.0.0
@ -9,3 +13,4 @@ dashboard: local=8443 remote=443 ns=kubernetes-dashboard svc=kubernetes-dashboar
postgres: local=5432 remote=5432 ns=prole-db001 svc=prole-db-rw address=0.0.0.0
prometheus: local=9090 remote=9090 ns=monitoring svc=kps-kube-prometheus-stack-prometheus address=127.0.0.1
grafana: local=3000 remote=80 ns=monitoring svc=kps-grafana address=0.0.0.0
supabase-kong: local=8000 remote=8000 ns=supabase svc=kong address=0.0.0.0

View File

@ -1,15 +1,15 @@
; Prole Master Configuration File
; Generated by install.py on 2026-03-01 19:22:14
; Generated by install.py on 2026-03-02 00:06:12
; This file is used as input for Ansible deployment and k8s cluster creation.
[User]
; User-editable values; derived values below reference these by default.
NAMESPACE = prole-db001
PROLE_CONF = /Users/chrisfu/dev/prole/conf
PROLE_CONF = ${PROLE_HOME}/conf
PROLE_DATA = /Users/chrisfu/.prole/data
PROLE_HOME = mock_val
PROLE_HOME = /Users/chrisfu/dev/prole
PROLE_LOGS = /opt/prole/logs/chrisfu
PROLE_SERVICE = /Users/chrisfu/dev/prole/etc
PROLE_SERVICE = ${PROLE_HOME}/etc
SERVICE_NAMESPACE = default
[Inputs]
@ -17,19 +17,19 @@ SERVICE_NAMESPACE = default
build.deploy_env = Dev
build.run_build = false
database_options.distribution = percona
database_options.ext.dblink = True
database_options.ext.pg_buffercache = True
database_options.ext.pg_freespacemap = True
database_options.ext.pg_repack = True
database_options.ext.pg_stat_monitor = True
database_options.ext.pg_stat_statements = True
database_options.ext.pgaudit = True
database_options.ext.pgbadger = True
database_options.ext.pgcrypto = True
database_options.ext.pgrowlocks = True
database_options.ext.pgvector = True
database_options.ext.postgis = True
database_options.ext.postgres_fdw = True
database_options.ext.dblink = true
database_options.ext.pg_buffercache = true
database_options.ext.pg_freespacemap = true
database_options.ext.pg_repack = true
database_options.ext.pg_stat_monitor = true
database_options.ext.pg_stat_statements = true
database_options.ext.pgaudit = true
database_options.ext.pgbadger = true
database_options.ext.pgcrypto = true
database_options.ext.pgrowlocks = true
database_options.ext.pgvector = true
database_options.ext.postgis = true
database_options.ext.postgres_fdw = true
database_options.version_type = v18
dependencies.ansible.install = true
dependencies.auto_install_missing = true
@ -42,7 +42,7 @@ dependencies.opentofu.install = true
dependencies.python.install = true
dependencies.verify_all = false
disk_selection.disk_type = local
disk_selection.local_path = /Users/chrisfu
disk_selection.local_path = ${PROLE_HOME}/prole-tools-app/dist
disk_selection.removable_mount =
env_setup.NAMESPACE = ${NAMESPACE}
env_setup.PROLE_CONF = ${PROLE_CONF}
@ -52,14 +52,14 @@ env_setup.PROLE_LOGS = ${PROLE_LOGS}
env_setup.PROLE_SERVICE = ${PROLE_SERVICE}
init_cluster.at_rest_encryption_enabled = true
init_cluster.cluster_env = dev
init_cluster.deployment_target = prole-dev-cluster
init_cluster.deployment_target = knoe-dev-cluster
init_cluster.gitops_enabled = false
init_cluster.k3s_server_url =
init_cluster.k3s_token =
init_cluster.kerberos_enabled = false
init_cluster.k3s_server_url = https://myrddin.prole.org:6443
init_cluster.k3s_token = ${OPENBAO:kv/prole/prole-db001/k3s#token}
init_cluster.kerberos_enabled = true
init_cluster.mode = k3d
init_cluster.start_cluster = true
init_cluster.supabase_enabled = false
init_cluster.supabase_enabled = true
init_cnpg_deploy.force_rollout = false
init_cnpg_deploy.run_deploy = true
init_db_build.run_build = true
@ -67,15 +67,15 @@ init_password.db_host_port = 5432
init_password.db_namespace = ${NAMESPACE}
init_password.db_password = ${OPENBAO:kv/prole/prole-db001/db#password}
init_password.db_password_confirm = ${OPENBAO:kv/prole/prole-db001/db#password}
init_password.db_username = root
init_password.db_username = chrisfu
init_password.generate_ssh_key = true
init_scripts.run_scripts = true
kerberos_config.enabled = False
kerberos_config.enabled = true
kerberos_config.init_authority = false
kerberos_config.kdc = 10.0.0.205
kerberos_config.kdc = 10.0.0.3
kerberos_config.password = ${OPENBAO:kv/prole/prole-db001/kerberos#password}
kerberos_config.realm =
kerberos_config.test_connection = false
kerberos_config.realm = PROLE.ORG
kerberos_config.test_connection = true
kerberos_config.user = administrator
network_scan.run = true
ollama_config.model =
@ -92,9 +92,9 @@ DEPLOYMENT_MODE = k3d
DEPLOYMENT_TARGET = knoe-dev-cluster
DOCKER_IMPORT_DIR =
NAMESPACE = ${NAMESPACE}
PROLE_DB_USER = root
PROLE_DB_USER = chrisfu
PROLE_HOME = ${PROLE_HOME}
PROLE_K3S_SERVER =
PROLE_K3S_SERVER = https://myrddin.prole.org:6443
PROLE_K3S_TOKEN = ${OPENBAO:kv/prole/prole-db001/k3s#token}
PROLE_OPENTOFU_URL = http://127.0.0.1:8080
REGISTRY_NAMESPACE = default
@ -104,22 +104,26 @@ SERVICE_NAMESPACE = default
; No configuration values captured yet for this section.
[Dependencies]
STATUS = All installed
; No configuration values captured yet for this section.
[Network]
AD_DC_HOST = myrddin.prole.org
AD_DC_IP = 10.0.0.3
ANSIBLE_DOMAIN = prole.org
ANSIBLE_INFRASTRUCTURE = /Users/chrisfu/dev/prole/infrastructure
ANSIBLE_INVENTORY = /Users/chrisfu/dev/prole/infrastructure/inventory
ANSIBLE_INFRASTRUCTURE = ${PROLE_HOME}/infrastructure
ANSIBLE_INVENTORY = ${PROLE_HOME}/infrastructure/inventory
ANSIBLE_REALM = PROLE.ORG
ANSIBLE_TOPOLOGY = {"domain":"prole.org","realm":"PROLE.ORG","internal_records":{"aventage.prole.org":"10.0.0.206","fairyland.prole.org":"10.0.0.208","loghost.prole.org":"10.0.0.3","morana.prole.org":"10.0.0.66","morgoth.prole.org":"10.0.0.204","myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207","synology.prole.org":"10.0.0.203","zinfandel.prole.org":"10.0.0.205"},"ad_dc":{"host":"myrddin.prole.org","ip":"10.0.0.3"},"k3s":{"server_url":"https://myrddin.prole.org:6443","server_host":"myrddin.prole.org","token_present":true},"groups":{"iscsi":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org"],"pihole":["pi.prole.org","raspberry.prole.org"],"ad_dc":["myrddin.prole.org"],"k3s_hosts":["myrddin.prole.org","pi.prole.org","retropie.prole.org"],"linux_hosts":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org"]},"hosts":{"myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207"},"unmapped_hosts":[]}
KDC_ANSIBLE_DETECTED = 10.0.0.3
KDC_AUTO_DETECTED = 10.0.0.205
KERBEROS_AUTO_ENABLED = False
KDC_AUTO_DETECTED = 10.0.0.3
KERBEROS_AUTO_ENABLED = True
[Port Forwards]
PORT_FORWARD_K3D_MAPPING_1 = id=argocd;namespace=argocd;target=svc/argocd-server;address=0.0.0.0;hostPort=8081;servicePort=80;protocol=TCP;description=ArgoCD
PORT_FORWARD_K3D_MAPPING_10 = id=supabase-studio;namespace=supabase;target=svc/studio;address=0.0.0.0;hostPort=18080;servicePort=3000;protocol=TCP;description=Supabase Studio
PORT_FORWARD_K3D_MAPPING_11 = id=supabase-auth;namespace=supabase;target=svc/auth;address=127.0.0.1;hostPort=9999;servicePort=9999;protocol=TCP;description=Supabase Auth (GoTrue)
PORT_FORWARD_K3D_MAPPING_12 = id=supabase-rest;namespace=supabase;target=svc/rest;address=0.0.0.0;hostPort=3001;servicePort=3000;protocol=TCP;description=Supabase REST (PostgREST)
PORT_FORWARD_K3D_MAPPING_13 = id=supabase-realtime;namespace=supabase;target=svc/realtime;address=0.0.0.0;hostPort=4000;servicePort=4000;protocol=TCP;description=Supabase Realtime
PORT_FORWARD_K3D_MAPPING_2 = id=garage;namespace=default;target=svc/garage;address=0.0.0.0;hostPort=3900;servicePort=3900;protocol=TCP;description=Garage S3
PORT_FORWARD_K3D_MAPPING_3 = id=openbao;namespace=default;target=svc/openbao;address=127.0.0.1;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenBao
PORT_FORWARD_K3D_MAPPING_4 = id=opentofu;namespace=default;target=svc/opentofu;address=0.0.0.0;hostPort=8080;servicePort=8080;protocol=TCP;description=OpenTofu
@ -127,10 +131,9 @@ PORT_FORWARD_K3D_MAPPING_5 = id=dashboard;namespace=kubernetes-dashboard;target=
PORT_FORWARD_K3D_MAPPING_6 = id=postgres;namespace=${NAMESPACE};target=svc/prole-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL (primary)
PORT_FORWARD_K3D_MAPPING_7 = id=prometheus;namespace=monitoring;target=svc/kps-kube-prometheus-stack-prometheus;address=127.0.0.1;hostPort=9090;servicePort=9090;protocol=TCP;description=Prometheus UI
PORT_FORWARD_K3D_MAPPING_8 = id=grafana;namespace=monitoring;target=svc/kps-grafana;address=0.0.0.0;hostPort=3000;servicePort=80;protocol=TCP;description=Grafana UI
STATUS = Running
PORT_FORWARD_K3D_MAPPING_9 = id=supabase-kong;namespace=supabase;target=svc/kong;address=0.0.0.0;hostPort=8000;servicePort=8000;protocol=TCP;description=Supabase API (Kong)
[System Environment]
NAMESPACE = prole-db001
PROLE_CONF = ${PROLE_CONF}
PROLE_DATA = ${PROLE_DATA}
PROLE_HOME = ${PROLE_HOME}
@ -141,8 +144,18 @@ PROLE_SERVICE = ${PROLE_SERVICE}
; No configuration values captured yet for this section.
[Kerberos Authentication]
AD_PORT_FORWARD = 1
AD_PROXY_HOST_NETWORK = 1
AD_PROXY_IMAGE = alpine/socat
AD_PROXY_SERVICE = prole-kerberos-ad-dc
AD_TCP_PORTS = 88 389 445 464 636
AD_UDP_PORTS = 88 464
ENABLED = True
KDC = 10.0.0.3
PASSWORD = ${OPENBAO:kv/prole/prole-db001/kerberos#password}
STATUS = Skipped
REALM = PROLE.ORG
SERVER = 10.0.0.3
USER = administrator
[Ollama]
; No configuration values captured yet for this section.
@ -150,19 +163,18 @@ STATUS = Skipped
[Optional Features]
AT_REST_ENCRYPTION_ENABLED = true
GITOPS_ENABLED = false
KERBEROS_ENABLED = false
SUPABASE_ENABLED = false
KERBEROS_ENABLED = true
SUPABASE_ENABLED = true
[GitOps]
STATUS = Skipped
[Database Creation]
DB_NAME = prole-db001
DB_USER = root
NAMESPACE = ${NAMESPACE}
[Initialize Cluster]
; No configuration values captured yet for this section.
ENVIRONMENT = knoe-dev-cluster
K3S_SERVER_URL = https://myrddin.prole.org:6443
K3S_TOKEN = ${OPENBAO:kv/prole/prole-db001/k3s#token}
[Dev Cluster (k3d)]
CLUSTER_ENV = k3d-knoe-dev-cluster
@ -173,7 +185,7 @@ MODE = k3d
[Service Cluster (k3s)]
CLUSTER_ENV = prole-service-cluster
DISPLAY_NAME = prole-service-cluster
K3S_SERVER_URL =
K3S_SERVER_URL = https://myrddin.prole.org:6443
K3S_TOKEN = ${OPENBAO:kv/prole/prole-db001/k3s#token}
MODE = k3s
PIPELINE_URL = http://127.0.0.1:8080
@ -186,14 +198,15 @@ MODE = k8s
PIPELINE_URL = http://127.0.0.1:8080
[Docker Build]
; No configuration values captured yet for this section.
LOCAL_REGISTRY = localhost:5000
LOCAL_REGISTRY_INTERNAL = k3d-prole-registry.localhost:5000
[Initialization Scripts]
; No configuration values captured yet for this section.
STATUS = Completed
[Deployment]
MODE = k3d
TARGET = prole-dev-cluster
TARGET = knoe-dev-cluster
[Install]
STATUS = Finished
; No configuration values captured yet for this section.

View File

@ -1046,13 +1046,13 @@ wait_for_backup() {
}
run_garage_backup() {
if [[ ! -x "$SCRIPT_DIR/init_prole-db-backup.sh" ]]; then
echo "WARN: init_prole-db-backup.sh not found; skipping Garage backup." >&2
if [[ ! -x "$SCRIPT_DIR/init_cnpg_backup.sh" ]]; then
echo "WARN: init_cnpg_backup.sh not found; skipping Garage backup." >&2
return 1
fi
echo "Running Garage backup via init_prole-db-backup.sh ..."
if ! "$SCRIPT_DIR/init_prole-db-backup.sh" start; then
echo "Running Garage backup via init_cnpg_backup.sh ..."
if ! "$SCRIPT_DIR/init_cnpg_backup.sh" start; then
echo "Garage backup script failed." >&2
return 1
fi

View File

@ -2,7 +2,7 @@
set -euo pipefail
# init_prole-db-backup.sh
# init_cnpg_backup.sh
# Purpose:
# - Configure CloudNative-PG to backup to Garage (S3-compatible) via Barman Cloud Plugin
# - Create initial backup
@ -44,6 +44,9 @@ RUN_FIRST_BACKUP=${RUN_FIRST_BACKUP:-1}
RETENTION_POLICY=${RETENTION_POLICY:-30d}
BARMAN_PLUGIN_NAME=${BARMAN_PLUGIN_NAME:-barman-cloud.cloudnative-pg.io}
BARMAN_OBJECT_NAME=${BARMAN_OBJECT_NAME:-prole-db-barman-objectstore}
BACKUP_STATUS_TIMEOUT=${BACKUP_STATUS_TIMEOUT:-600}
BACKUP_STATUS_INTERVAL=${BACKUP_STATUS_INTERVAL:-10}
PLUGIN_READY_TIMEOUT=${PLUGIN_READY_TIMEOUT:-180}
usage() {
cat <<USAGE
@ -52,7 +55,7 @@ Usage: $0 [start|backup|status]
Actions:
start Configure Garage-backed backups and run initial backup
backup [full|incr] Trigger a new backup now (default: full)
status Show backup resources
status Show backup resources and verify at least one successful base backup
USAGE
exit 1
}
@ -238,6 +241,85 @@ remove_native_barman_config() {
kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{\"spec\":{\"backup\":null}}" >/dev/null 2>&1 || true
}
wait_for_plugin_ready() {
local start_time now plugin_names deployment_rows ns ready replicas
start_time=$(date +%s)
while true; do
plugin_names=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{.spec.plugins[*].name}' 2>/dev/null || true)
if printf '%s\n' "$plugin_names" | tr ' ' '\n' | grep -Fxq "$BARMAN_PLUGIN_NAME"; then
deployment_rows=$(kubectl get deployment -A -l app.kubernetes.io/name=barman-cloud -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.status.readyReplicas}{"\t"}{.status.replicas}{"\n"}{end}' 2>/dev/null || true)
if [[ -z "$deployment_rows" ]]; then
deployment_rows=$(kubectl get deployment -A -o jsonpath='{range .items[?(@.metadata.name=="barman-cloud")]}{.metadata.namespace}{"\t"}{.status.readyReplicas}{"\t"}{.status.replicas}{"\n"}{end}' 2>/dev/null || true)
fi
while IFS=$'\t' read -r ns ready replicas; do
[[ -z "$ns" ]] && continue
ready=${ready:-0}
replicas=${replicas:-0}
if (( ready >= 1 && replicas >= 1 )); then
return 0
fi
done <<< "$deployment_rows"
fi
now=$(date +%s)
if (( now - start_time >= PLUGIN_READY_TIMEOUT )); then
echo "ERROR: Timed out waiting for CNPG plugin '$BARMAN_PLUGIN_NAME' to become available." >&2
return 1
fi
echo "Waiting for CNPG plugin '$BARMAN_PLUGIN_NAME' to become available ..."
sleep 5
done
}
has_successful_base_backup() {
local rows name phase method backup_type phase_lc method_lc backup_type_lc
rows=$(kubectl get backup -n "$NAMESPACE" -o jsonpath="{range .items[?(@.spec.cluster.name=='$CNPG_CLUSTER_NAME')]}{.metadata.name}{\"\t\"}{.status.phase}{\"\t\"}{.spec.method}{\"\t\"}{.spec.pluginConfiguration.parameters.backupType}{\"\n\"}{end}" 2>/dev/null || true)
if [[ -z "$rows" ]]; then
return 1
fi
while IFS=$'\t' read -r name phase method backup_type; do
[[ -z "$name" ]] && continue
phase_lc=$(echo "${phase:-}" | tr '[:upper:]' '[:lower:]')
method_lc=$(echo "${method:-}" | tr '[:upper:]' '[:lower:]')
backup_type_lc=$(echo "${backup_type:-}" | tr '[:upper:]' '[:lower:]')
if [[ "$phase_lc" != "completed" && "$phase_lc" != "succeeded" ]]; then
continue
fi
if [[ "$method_lc" == "plugin" ]] && [[ "$backup_type_lc" == "incremental" || "$backup_type_lc" == "incr" ]]; then
continue
fi
return 0
done <<< "$rows"
return 1
}
wait_for_successful_base_backup() {
local start_time now elapsed
start_time=$(date +%s)
while true; do
if has_successful_base_backup; then
return 0
fi
now=$(date +%s)
elapsed=$((now - start_time))
if (( elapsed >= BACKUP_STATUS_TIMEOUT )); then
return 1
fi
echo "Waiting for a successful base backup ... (${elapsed}s/${BACKUP_STATUS_TIMEOUT}s)"
sleep "$BACKUP_STATUS_INTERVAL"
done
}
trigger_backup() {
local backup_type="${1:-full}"
local backup_name
@ -281,6 +363,14 @@ status() {
ensure_tools
echo "Backups for cluster '$CNPG_CLUSTER_NAME' in namespace '$NAMESPACE':"
kubectl get backup -n "$NAMESPACE" | grep "$CNPG_CLUSTER_NAME" || true
if has_successful_base_backup; then
echo "Backup status: OK (at least one successful base backup is available)."
return 0
fi
echo "ERROR: No successful base backup found for cluster '$CNPG_CLUSTER_NAME'." >&2
return 1
}
case "$ACTION" in
@ -291,10 +381,17 @@ case "$ACTION" in
ensure_garage_bucket_and_key
apply_barman_object_store
ensure_barman_plugin_config
wait_for_plugin_ready
remove_native_barman_config
if [[ "$RUN_FIRST_BACKUP" == "1" ]]; then
trigger_backup
fi
if ! wait_for_successful_base_backup; then
echo "ERROR: Timed out waiting for a successful base backup." >&2
status || true
exit 1
fi
status
;;
backup)
ensure_tools

View File

@ -89,6 +89,7 @@ ARGOCD_NS="${ARGOCD_NAMESPACE:-argocd}"
REGISTRY_NS="${REGISTRY_NAMESPACE:-default}"
prole_ensure_kubeconfig >/dev/null 2>&1 || true
prole_ensure_kube_context || exit 1
# Check cluster reachability early to fail fast
if [[ "$ACTION" != "status" ]]; then

View File

@ -1,256 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
PROG="init_gitea"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh"
MODE="$(prole_normalize_mode "${PROLE_MODE:-${DEPLOYMENT_MODE:-k3d}}")"
NAMESPACE="${GITEA_NAMESPACE:-}"
CFG_PATH=""
FORCE=0
usage() {
cat <<EOF
Usage:
$PROG [options] [deploy]
Options:
--mode <k3d|k3s|k8s|local> Deployment mode (default: ${MODE:-k3d})
-n, --namespace <name> Target namespace (default: gitea)
-c, --config <prole.cfg> Path to prole.cfg (defaults to detected)
--force Delete existing release before deploy
--help Show this help
Behavior:
- Prefers Helm install/upgrade (gitea-charts/gitea).
- Falls back to applying a generated Deployment/Service/Ingress manifest.
- If a local registry is enabled, the image is mirrored there first.
EOF
}
die() { echo "[ERROR] $*" >&2; exit 2; }
log() { echo "[INFO] $*"; }
warn() { echo "[WARN] $*" >&2; }
while [[ $# -gt 0 ]]; do
case "$1" in
--mode) MODE="$(prole_normalize_mode "${2:-}")"; shift 2 ;;
--mode=*) MODE="$(prole_normalize_mode "${1#*=}")"; shift 1 ;;
-n|--namespace) NAMESPACE="${2:-}"; shift 2 ;;
--namespace=*) NAMESPACE="${1#*=}"; shift 1 ;;
-c|--config) CFG_PATH="${2:-}"; shift 2 ;;
--config=*) CFG_PATH="${1#*=}"; shift 1 ;;
--force) FORCE=1; shift ;;
-h|--help) usage; exit 0 ;;
*) break ;;
esac
done
# Resolve config path and namespace defaults from prole.cfg when present
if [[ -z "$CFG_PATH" && -n "${PROLE_CONF:-}" && -f "${PROLE_CONF}/prole.cfg" ]]; then
CFG_PATH="${PROLE_CONF}/prole.cfg"
elif [[ -z "$CFG_PATH" && -f "$SCRIPT_DIR/../conf/prole.cfg" ]]; then
CFG_PATH="$SCRIPT_DIR/../conf/prole.cfg"
fi
if [[ -z "$NAMESPACE" && -n "$CFG_PATH" ]]; then
maybe_ns="$(_prole_cfg_extract_key "$CFG_PATH" "GITOPS_NAMESPACE")"
[[ -z "$maybe_ns" ]] && maybe_ns="$(_prole_cfg_extract_key "$CFG_PATH" "GITEA_NAMESPACE")"
NAMESPACE="$maybe_ns"
fi
NAMESPACE="${NAMESPACE:-gitea}"
export GITEA_NAMESPACE="$NAMESPACE"
case "$MODE" in
k3d|k3s|k8s|local) ;;
*) die "Unsupported mode '$MODE' (use k3d, k3s, k8s, or local)" ;;
esac
export PROLE_MODE="$MODE"
command -v kubectl >/dev/null || die "kubectl not found"
command -v helm >/dev/null || warn "helm not found — will use manifest fallback"
kubectl get ns "$NAMESPACE" >/dev/null 2>&1 || kubectl create namespace "$NAMESPACE" >/dev/null 2>&1
CHART_REPO="https://dl.gitea.com/charts"
CHART_NAME="gitea-charts/gitea"
RELEASE_NAME="gitea"
IMAGE_REPO_DEFAULT="gitea/gitea"
IMAGE_TAG="${GITEA_IMAGE_TAG:-1.22.3}"
IMAGE_REPO="$IMAGE_REPO_DEFAULT"
# Optional reset
if [[ "$FORCE" -eq 1 ]]; then
warn "--force specified; removing existing Gitea resources in namespace $NAMESPACE"
if command -v helm >/dev/null 2>&1; then
helm uninstall "$RELEASE_NAME" -n "$NAMESPACE" >/dev/null 2>&1 || true
fi
kubectl -n "$NAMESPACE" delete deploy/gitea svc/gitea-http svc/gitea-ssh ingress/gitea >/dev/null 2>&1 || true
fi
# Mirror to local registry if enabled
if _prole_local_registry_enabled && command -v docker >/dev/null; then
HOST_REG="${LOCAL_REGISTRY:-${PROLE_LOCAL_REGISTRY:-}}"
INTERNAL_REG="${LOCAL_REGISTRY_INTERNAL:-${PROLE_LOCAL_REGISTRY_INTERNAL:-}}"
if [[ -n "$HOST_REG" ]]; then
HOST_IMG="${HOST_REG}/${IMAGE_REPO_DEFAULT}"
log "Pulling ${IMAGE_REPO_DEFAULT}:${IMAGE_TAG}"
docker pull "${IMAGE_REPO_DEFAULT}:${IMAGE_TAG}" >/dev/null
log "Tagging ${HOST_IMG}:${IMAGE_TAG}"
docker tag "${IMAGE_REPO_DEFAULT}:${IMAGE_TAG}" "${HOST_IMG}:${IMAGE_TAG}"
log "Pushing ${HOST_IMG}:${IMAGE_TAG}"
docker push "${HOST_IMG}:${IMAGE_TAG}" >/dev/null || warn "Push to local registry failed; continuing with upstream image"
if [[ -n "$INTERNAL_REG" ]]; then
IMAGE_REPO="${INTERNAL_REG}/gitea/gitea"
else
IMAGE_REPO="${HOST_REG}/gitea/gitea"
fi
fi
fi
VALUES_FILE="$(mktemp /tmp/gitea-values.XXXXXX.yaml)"
cat >"$VALUES_FILE" <<EOF
image:
repository: "${IMAGE_REPO}"
tag: "${IMAGE_TAG}"
service:
http:
type: ClusterIP
port: 3000
ssh:
type: ClusterIP
port: 22
gitea:
config:
server:
DOMAIN: ${GITEA_DOMAIN:-git.prole.org}
SSH_DOMAIN: ${GITEA_SSH_DOMAIN:-git.prole.org}
ROOT_URL: "http://${GITEA_DOMAIN:-git.prole.org}:3000/"
ingress:
enabled: ${GITEA_INGRESS_ENABLED:-$( [[ "$MODE" == "k3s" || "$MODE" == "k8s" ]] && echo true || echo false )}
className: ${GITEA_INGRESS_CLASS:-nginx}
hosts:
- host: ${GITEA_DOMAIN:-git.prole.org}
paths:
- path: /
pathType: Prefix
backend:
service:
name: ${RELEASE_NAME}-http
port:
number: 3000
EOF
helm_deployed=0
if command -v helm >/dev/null; then
if ! helm repo list 2>/dev/null | grep -q "gitea-charts"; then
helm repo add gitea-charts "$CHART_REPO" >/dev/null 2>&1 || warn "helm repo add failed"
fi
helm repo update >/dev/null 2>&1 || true
log "Deploying Gitea via Helm to namespace ${NAMESPACE} (mode=${MODE})"
set +e
helm upgrade --install "$RELEASE_NAME" "$CHART_NAME" \
--namespace "$NAMESPACE" \
--create-namespace \
-f "$VALUES_FILE"
rc=$?
set -e
if [[ $rc -eq 0 ]]; then
helm_deployed=1
log "Helm deployment successful."
else
warn "Helm deployment failed (code $rc); falling back to manifests."
fi
fi
if [[ $helm_deployed -eq 0 ]]; then
MANIFEST_FILE="$(mktemp /tmp/gitea-manifest.XXXXXX.yaml)"
cat >"$MANIFEST_FILE" <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitea
spec:
replicas: 1
selector:
matchLabels:
app: gitea
template:
metadata:
labels:
app: gitea
spec:
containers:
- name: gitea
image: ${IMAGE_REPO}:${IMAGE_TAG}
ports:
- name: http
containerPort: 3000
- name: ssh
containerPort: 22
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: gitea-http
spec:
selector:
app: gitea
ports:
- name: http
port: 3000
targetPort: 3000
---
apiVersion: v1
kind: Service
metadata:
name: gitea-ssh
spec:
selector:
app: gitea
ports:
- name: ssh
port: 22
targetPort: 22
EOF
if [[ "$MODE" == "k3s" || "$MODE" == "k8s" ]]; then
cat >>"$MANIFEST_FILE" <<EOF
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gitea
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "64m"
spec:
ingressClassName: nginx
rules:
- host: git.prole.org
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gitea-http
port:
number: 3000
EOF
fi
log "Applying fallback manifest"
kubectl -n "$NAMESPACE" apply -f "$MANIFEST_FILE"
fi
# Ensure port-forward hints / conflicts handled by caller via _build_required_port_forwards
log "Gitea deployment completed in namespace '$NAMESPACE'."

View File

@ -26,6 +26,7 @@ elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
fi
prole_ensure_kubeconfig >/dev/null 2>&1 || true
prole_ensure_kube_context || exit 1
ACTION=${1:-initialize}

View File

@ -328,7 +328,6 @@ resolve_grafana_password() {
GRAFANA_ADMIN_PASSWORD="$fetched"
fi
fi
if [[ -z "${GRAFANA_ADMIN_PASSWORD:-}" || "${GRAFANA_ADMIN_PASSWORD}" == '${OPENBAO:'* || "${GRAFANA_ADMIN_PASSWORD}" == '${PROLE_SECRET:'* ]]; then
local db_pw="${DB_PASSWORD:-}"
if [[ -z "$db_pw" || "$db_pw" == '${OPENBAO:'* || "$db_pw" == '${PROLE_SECRET:'* ]]; then
@ -342,6 +341,10 @@ resolve_grafana_password() {
GRAFANA_ADMIN_PASSWORD="$db_pw"
fi
fi
# Strip any trailing newlines/carriage-returns that may have crept in via
# prole.cfg parsing, file reads, or shell substitution edge-cases.
GRAFANA_ADMIN_PASSWORD="${GRAFANA_ADMIN_PASSWORD//$'\n'/}"
GRAFANA_ADMIN_PASSWORD="${GRAFANA_ADMIN_PASSWORD//$'\r'/}"
}
write_grafana_password_to_openbao() {
@ -351,9 +354,12 @@ write_grafana_password_to_openbao() {
if [[ -z "$token" || -z "$url" || -z "${GRAFANA_ADMIN_PASSWORD:-}" ]]; then
return 0
fi
local _clean_grafana_pw
_clean_grafana_pw="${GRAFANA_ADMIN_PASSWORD//$'\n'/}"
_clean_grafana_pw="${_clean_grafana_pw//$'\r'/}"
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$url/v1/kv/data/prole/${NAMESPACE:-default}/monitoring" \
-d "{\"data\":{\"grafana_admin_password\":\"$GRAFANA_ADMIN_PASSWORD\"}}" >/dev/null || true
-d "{\"data\":{\"grafana_admin_password\":\"$_clean_grafana_pw\"}}" >/dev/null || true
}
cleanup_grafana_rbac_conflicts() {
@ -433,6 +439,9 @@ install_monitoring() {
if [[ -z "${GRAFANA_ADMIN_PASSWORD:-}" ]]; then
GRAFANA_ADMIN_PASSWORD="admin" # Fallback
fi
# Belt-and-suspenders: ensure no newline survives into the Helm values YAML.
GRAFANA_ADMIN_PASSWORD="${GRAFANA_ADMIN_PASSWORD//$'\n'/}"
GRAFANA_ADMIN_PASSWORD="${GRAFANA_ADMIN_PASSWORD//$'\r'/}"
local values_file
values_file=$(mktemp)

View File

@ -61,6 +61,7 @@ if [[ -n "$MODE" && "$MODE" != "k3d" ]]; then
fi
prole_ensure_kubeconfig >/dev/null 2>&1 || true
prole_ensure_kube_context || exit 1
PORT_MAPPING_FILE_PATH="${PORT_MAPPING_FILE:-}"
if [[ -z "$PORT_MAPPING_FILE_PATH" ]]; then

View File

@ -652,9 +652,12 @@ init_openbao_kv_and_store_admin_key() {
if [[ -n "${GRAFANA_ADMIN_PASSWORD:-}" ]]; then
echo "Writing Grafana admin password to kv/$BAO_PATH_MONITORING ..."
local _clean_grafana_pw
_clean_grafana_pw="${GRAFANA_ADMIN_PASSWORD//$'\n'/}"
_clean_grafana_pw="${_clean_grafana_pw//$'\r'/}"
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/$BAO_PATH_MONITORING" \
-d "{\"data\":{\"grafana_admin_password\":\"$GRAFANA_ADMIN_PASSWORD\"}}" >/dev/null
-d "{\"data\":{\"grafana_admin_password\":\"$_clean_grafana_pw\"}}" >/dev/null
echo "Stored Grafana admin password in OpenBao kv/$BAO_PATH_MONITORING."
fi
}

View File

@ -162,15 +162,15 @@ run_barman_backup() {
return 1
fi
if [[ -x "$SCRIPT_DIR/init_prole-db-backup.sh" ]]; then
if [[ -x "$SCRIPT_DIR/init_cnpg_backup.sh" ]]; then
log "Running barman backup (Garage bucket: $GARAGE_BACKUP_BUCKET) ..."
NAMESPACE="$NAMESPACE" \
GARAGE_BACKUP_BUCKET="$GARAGE_BACKUP_BUCKET" \
GARAGE_BACKUP_SECRET_NAME="$GARAGE_BACKUP_SECRET_NAME" \
GARAGE_S3_ENDPOINT="$GARAGE_S3_ENDPOINT" \
RUN_FIRST_BACKUP=1 "$SCRIPT_DIR/init_prole-db-backup.sh" start
RUN_FIRST_BACKUP=1 "$SCRIPT_DIR/init_cnpg_backup.sh" start
else
err "init_prole-db-backup.sh not found; cannot run backup."
err "init_cnpg_backup.sh not found; cannot run backup."
return 1
fi

View File

@ -29,6 +29,7 @@ common_core_preparse_config "$@"
source "$SCRIPT_DIR/prole_cfg.sh"
prole_ensure_kubeconfig >/dev/null 2>&1 || true
prole_ensure_kube_context || exit 1
ACTION=""
SERVICE_NAMESPACE_OVERRIDE=""

View File

@ -645,6 +645,64 @@ prole_ensure_kubeconfig() {
_prole_kubeconfig_maybe_insecure
}
# Validate and switch the active kubectl context to match PROLE_MODE set in prole.cfg.
# prole.cfg always overrides the local environment.
# Call this after prole_ensure_kubeconfig in every init_*.sh script.
# Usage: prole_ensure_kube_context
prole_ensure_kube_context() {
local mode
mode="${PROLE_MODE:-}"
if [[ -z "$mode" ]]; then
return 0
fi
local current_context
current_context=$(kubectl config current-context 2>/dev/null || true)
case "$mode" in
k3d)
# k3d contexts are always named k3d-<cluster_name>
if [[ "$current_context" == k3d-* ]]; then
return 0
fi
local cluster_name target_context
cluster_name="${K3D_CLUSTER_NAME:-knoe-dev-cluster}"
target_context="k3d-${cluster_name}"
echo "WARN: prole.cfg mode is 'k3d' but current kubectx is '${current_context:-<none>}'" >&2
echo "INFO: Switching kubectl context to '${target_context}' (prole.cfg overrides local env)" >&2
if kubectl config use-context "$target_context" >/dev/null 2>&1; then
echo "INFO: kubectl context set to '${target_context}'" >&2
else
echo "ERROR: Could not switch to context '${target_context}'. Available contexts:" >&2
kubectl config get-contexts --no-headers 2>/dev/null | awk '{print " "$2}' >&2 || true
return 1
fi
;;
k3s|k8s)
# k3s/k8s contexts must NOT be a k3d context
if [[ "$current_context" != k3d-* ]]; then
return 0
fi
echo "WARN: prole.cfg mode is '${mode}' but current kubectx is '${current_context}' (a k3d context)" >&2
echo "INFO: Switching kubectl context to match KUBECONFIG='${KUBECONFIG:-<default>}' (prole.cfg overrides local env)" >&2
local target_context
target_context=$(kubectl config get-contexts --no-headers 2>/dev/null \
| awk '{print $2}' | grep -v '^k3d-' | head -1 || true)
if [[ -z "$target_context" ]]; then
echo "ERROR: No non-k3d context found in KUBECONFIG='${KUBECONFIG:-<default>}'. Available contexts:" >&2
kubectl config get-contexts --no-headers 2>/dev/null | awk '{print " "$2}' >&2 || true
return 1
fi
if kubectl config use-context "$target_context" >/dev/null 2>&1; then
echo "INFO: kubectl context set to '${target_context}'" >&2
else
echo "ERROR: Could not switch to context '${target_context}'" >&2
return 1
fi
;;
esac
}
if _prole_is_secret_ref "${PROLE_K3S_TOKEN:-}"; then
if _prole_is_openbao_ref "${PROLE_K3S_TOKEN:-}"; then
_decoded=$(_prole_resolve_openbao_ref "${PROLE_K3S_TOKEN:-}")

View File

@ -253,7 +253,7 @@ STATUS_SCRIPTS=(
"init_kong.sh"
"init_db_manager.sh"
"init_monitoring.sh"
"init_prole-db-backup.sh"
"init_cnpg_backup.sh"
"init_port_forwards.sh"
)

1142
gitea/deploy.sh Executable file

File diff suppressed because it is too large Load Diff

View File

@ -70,6 +70,7 @@ from installer.core.milestones import (
KerberosMilestone,
DeploymentMilestone,
GitOpsMilestone,
SupabaseImagePreloadMilestone,
SupabaseMilestone,
)
from installer.core.stream_exec import run_streaming_cmd
@ -3545,7 +3546,7 @@ class ProleSilentInstaller(ProleInstallerBase):
steps.append(("init_kerberos.sh", ["initialize"], False))
steps.extend(
[
("init_prole-db-backup.sh", ["start"], False),
("init_cnpg_backup.sh", ["start"], False),
("init_kong.sh", ["start"], False),
("init_monitoring.sh", ["initialize"], False),
]
@ -3897,6 +3898,7 @@ class ProleSilentInstaller(ProleInstallerBase):
InitializationScriptsMilestone(),
KerberosMilestone(),
GitOpsMilestone(),
SupabaseImagePreloadMilestone(),
SupabaseMilestone(),
DeploymentMilestone(),
]

View File

@ -663,7 +663,7 @@ class InitializationScriptsMilestone(Milestone):
scripts.extend(
[
"init_prole-db-backup.sh",
"init_cnpg_backup.sh",
"init_kong.sh",
"init_monitoring.sh",
]
@ -689,7 +689,7 @@ class InitializationScriptsMilestone(Milestone):
args.append("update")
elif script == "init_cloudnative_pg.sh":
args = mode_args + ["initialize"]
elif script == "init_prole-db-backup.sh":
elif script == "init_cnpg_backup.sh":
args = mode_args + ["start"]
elif script == "init_kong.sh":
args = mode_args + ["start"]
@ -902,6 +902,86 @@ class KerberosMilestone(Milestone):
raise Exception(msg)
def _resolve_supabase_deploy_mode(state: InstallerState) -> str:
from installer.core.env import _normalize_cluster_env
deploy_mode = (
os.environ.get("SUPABASE_DEPLOY_MODE") or os.environ.get("SUPABASE_MODE") or ""
).strip()
if deploy_mode:
return deploy_mode
cluster_env = _normalize_cluster_env(state.inputs.get("init_cluster.cluster_env", "dev"))
if cluster_env in ("service", "prod"):
return "k8s"
return "k3d"
class SupabaseImagePreloadMilestone(Milestone):
def __init__(self):
super().__init__("supabase_images_preload", "Supabase Image Preload")
self.logger = logging.getLogger("SupabaseImagePreloadMilestone")
def execute(
self, state: InstallerState, progress: ProgressCallback | None = None
) -> None:
enabled = self._parse_bool(
state.inputs.get("init_cluster.supabase_enabled", "False"),
default=False,
)
supa_sec = state.config_data.setdefault("Supabase", {})
if not enabled:
supa_sec["IMAGES_STATUS"] = "Skipped"
if progress:
progress("Supabase disabled; skipping image preload", 1.0)
return
from installer.core.env import _parse_bool
env = self._get_script_env(state)
script_path = state.controller.project_root / "supabase" / "deploy.sh"
if not script_path.exists():
self.logger.error(f"Supabase deploy script not found: {script_path}")
supa_sec["IMAGES_STATUS"] = "Attempted"
return
deploy_mode = _resolve_supabase_deploy_mode(state)
if deploy_mode != "k3d":
supa_sec["IMAGES_STATUS"] = "Skipped"
if progress:
progress(
f"Supabase image preload not required for mode '{deploy_mode}'",
1.0,
)
return
args = ["--mode", deploy_mode, "--prefetch-images-only"]
cfg_path = state.controller.project_root / "conf" / "prole.cfg"
if cfg_path.exists():
args.extend(["-c", str(cfg_path)])
if _parse_bool(os.environ.get("SUPABASE_USE_DEV_COMPOSE"), False):
args.append("--with-dev-helpers")
if progress:
progress("Preloading Supabase images...", 0.4)
self.logger.info(f"supabase/deploy.sh {' '.join(args)}")
rc = subprocess.run(
["bash", str(script_path)] + args,
env=env,
).returncode
if rc == 0:
supa_sec["IMAGES_STATUS"] = "Prepared"
state.inputs["supabase.images_preloaded"] = "True"
if progress:
progress("Supabase images preloaded", 1.0)
else:
supa_sec["IMAGES_STATUS"] = "Attempted"
msg = f"Supabase image preload failed (code {rc})"
self.logger.error(msg)
raise Exception(msg)
class SupabaseMilestone(Milestone):
def __init__(self):
super().__init__("supabase", "Supabase Deployment")
@ -921,7 +1001,7 @@ class SupabaseMilestone(Milestone):
progress("Supabase disabled; skipping", 1.0)
return
from installer.core.env import _normalize_cluster_env, _parse_bool
from installer.core.env import _parse_bool
env = self._get_script_env(state)
script_path = state.controller.project_root / "supabase" / "deploy.sh"
@ -930,19 +1010,7 @@ class SupabaseMilestone(Milestone):
supa_sec["STATUS"] = "Attempted"
return
deploy_mode = (
os.environ.get("SUPABASE_DEPLOY_MODE")
or os.environ.get("SUPABASE_MODE")
or ""
).strip()
if not deploy_mode:
cluster_env = _normalize_cluster_env(
state.inputs.get("init_cluster.cluster_env", "dev")
)
if cluster_env in ("service", "prod"):
deploy_mode = "k8s"
else:
deploy_mode = "k3d"
deploy_mode = _resolve_supabase_deploy_mode(state)
args = ["--mode", deploy_mode]
cfg_path = state.controller.project_root / "conf" / "prole.cfg"
@ -952,6 +1020,8 @@ class SupabaseMilestone(Milestone):
args.append("--with-dev-helpers")
if _parse_bool(os.environ.get("SUPABASE_FOREGROUND"), False):
args.append("--foreground")
if _parse_bool(state.inputs.get("supabase.images_preloaded", "False"), False):
args.append("--skip-prefetch")
self.logger.info(f"supabase/deploy.sh {' '.join(args)}")
rc = subprocess.run(

View File

@ -18,11 +18,11 @@ class ScreenBaseMixin:
def _render_paragraph(self, text, y, wrap=600):
ui.render_paragraph(self, text, y, wrap)
def _create_console_output(self, y=280, title="Scan Output", width=880, height=450):
def _create_console_output(self, y=280, title="Scan Output", width=880, height=450, x=48):
"""Create a standardized console output area with a label and scrollable text."""
# Section title/label for the console
ui.canvas_text(
self, 48, y, title, fill="#1d1d1f", font=("SF Pro Text", 12, "bold")
self, x, y, title, fill="#1d1d1f", font=("SF Pro Text", 12, "bold")
)
y_console = y + 30
@ -32,7 +32,7 @@ class ScreenBaseMixin:
console.pack(fill="both", expand=True, padx=1, pady=1)
console_window = self.bg_canvas.create_window(
48, y_console, window=console_bg, anchor="nw", width=width, height=height
x, y_console, window=console_bg, anchor="nw", width=width, height=height
)
self._canvas_items.append(console_window)
self._overlay_widgets.append(console_bg)

View File

@ -80,7 +80,7 @@ class SecurityScreenMixin:
status_item = ui.canvas_text(
self,
340,
250,
y + 6,
f"Status: {initial_status}",
fill="black",
@ -97,10 +97,14 @@ class SecurityScreenMixin:
self._kerberos_status_var.trace_add("write", _update_status_text)
x_field = 200
field_w = 400
left_col = content_width // 3
right_col_x = left_col + 16
right_col_w = content_width - right_col_x - 48
x_field = 160
field_w = left_col - x_field - 8
y += 60
console_y = y
ui.canvas_text(
self, 48, y, "Kerberos Realm:", fill="black", font=("SF Pro Text", 12)
)
@ -201,9 +205,9 @@ class SecurityScreenMixin:
self._canvas_items.append(btn_window)
self._overlay_widgets.append(test_btn)
y += 60
self._kerberos_console = self._create_console_output(
y=y, title="Kerberos Output", width=900, height=260
x=right_col_x, y=console_y, title="Kerberos Output",
width=right_col_w, height=350
)
def test_kerberos_connection(self):
@ -304,7 +308,10 @@ class SecurityScreenMixin:
if rc2 == 0:
write_line("\nKerberos test completed successfully.\n")
self.safe_after(
lambda: self._kerberos_status_var.set("Test Passed")
lambda: (
self._kerberos_status_var.set("Test Passed"),
self.next_button.configure(state="normal"),
)
)
else:
write_line(f"\nKerberos test failed with code {rc2}\n")

View File

@ -158,7 +158,7 @@ class ServicesScreenMixin:
("Common Services Log", "common_services_log"),
("CloudNative-PG", "init_cloudnative_pg.sh"),
]
scripts.append(("Prole DB Backup", "init_prole-db-backup.sh"))
scripts.append(("Prole DB Backup", "init_cnpg_backup.sh"))
scripts.append(("Kong API Gateway", "init_kong.sh"))
scripts.append(("Monitoring", "init_monitoring.sh"))
mode = self._deployment_mode()
@ -369,9 +369,9 @@ class ServicesScreenMixin:
"Skipping CloudNative-PG initialization because previous steps failed.\n"
)
# 4. init_prole-db-backup.sh start
# 4. init_cnpg_backup.sh start
if overall_success:
script = "init_prole-db-backup.sh"
script = "init_cnpg_backup.sh"
_select_tab(script)
self.script_consoles[script].clear()
self.script_consoles[script].write(f"Running {script} start...\n")
@ -412,7 +412,7 @@ class ServicesScreenMixin:
f"\n{script} completed successfully.\n"
)
else:
self.script_consoles["init_prole-db-backup.sh"].write(
self.script_consoles["init_cnpg_backup.sh"].write(
"Skipping Prole DB backup because previous steps failed.\n"
)

View File

@ -5,7 +5,7 @@ metadata:
name: prole-db
spec:
instances: 3
imageName: k3d-prole-registry:5000/prole-db:18-089
imageName: k3d-prole-registry:5000/prole-db:18-092
postgresUID: 100
postgresGID: 101
maxSyncReplicas: 1

View File

@ -4,7 +4,7 @@ metadata:
name: prole-db
spec:
instances: 3
imageName: k3d-prole-registry:5000/prole-db:18-089
imageName: k3d-prole-registry:5000/prole-db:18-092
postgresUID: 100
postgresGID: 101
maxSyncReplicas: 1

View File

@ -1046,13 +1046,13 @@ wait_for_backup() {
}
run_garage_backup() {
if [[ ! -x "$SCRIPT_DIR/init_prole-db-backup.sh" ]]; then
echo "WARN: init_prole-db-backup.sh not found; skipping Garage backup." >&2
if [[ ! -x "$SCRIPT_DIR/init_cnpg_backup.sh" ]]; then
echo "WARN: init_cnpg_backup.sh not found; skipping Garage backup." >&2
return 1
fi
echo "Running Garage backup via init_prole-db-backup.sh ..."
if ! "$SCRIPT_DIR/init_prole-db-backup.sh" start; then
echo "Running Garage backup via init_cnpg_backup.sh ..."
if ! "$SCRIPT_DIR/init_cnpg_backup.sh" start; then
echo "Garage backup script failed." >&2
return 1
fi

View File

@ -2,7 +2,7 @@
set -euo pipefail
# init_prole-db-backup.sh
# init_cnpg_backup.sh
# Purpose:
# - Configure CloudNative-PG to backup to Garage (S3-compatible) via Barman Cloud Plugin
# - Create initial backup
@ -44,6 +44,9 @@ RUN_FIRST_BACKUP=${RUN_FIRST_BACKUP:-1}
RETENTION_POLICY=${RETENTION_POLICY:-30d}
BARMAN_PLUGIN_NAME=${BARMAN_PLUGIN_NAME:-barman-cloud.cloudnative-pg.io}
BARMAN_OBJECT_NAME=${BARMAN_OBJECT_NAME:-prole-db-barman-objectstore}
BACKUP_STATUS_TIMEOUT=${BACKUP_STATUS_TIMEOUT:-600}
BACKUP_STATUS_INTERVAL=${BACKUP_STATUS_INTERVAL:-10}
PLUGIN_READY_TIMEOUT=${PLUGIN_READY_TIMEOUT:-180}
usage() {
cat <<USAGE
@ -52,7 +55,7 @@ Usage: $0 [start|backup|status]
Actions:
start Configure Garage-backed backups and run initial backup
backup [full|incr] Trigger a new backup now (default: full)
status Show backup resources
status Show backup resources and verify at least one successful base backup
USAGE
exit 1
}
@ -238,6 +241,85 @@ remove_native_barman_config() {
kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{\"spec\":{\"backup\":null}}" >/dev/null 2>&1 || true
}
wait_for_plugin_ready() {
local start_time now plugin_names deployment_rows ns ready replicas
start_time=$(date +%s)
while true; do
plugin_names=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{.spec.plugins[*].name}' 2>/dev/null || true)
if printf '%s\n' "$plugin_names" | tr ' ' '\n' | grep -Fxq "$BARMAN_PLUGIN_NAME"; then
deployment_rows=$(kubectl get deployment -A -l app.kubernetes.io/name=barman-cloud -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.status.readyReplicas}{"\t"}{.status.replicas}{"\n"}{end}' 2>/dev/null || true)
if [[ -z "$deployment_rows" ]]; then
deployment_rows=$(kubectl get deployment -A -o jsonpath='{range .items[?(@.metadata.name=="barman-cloud")]}{.metadata.namespace}{"\t"}{.status.readyReplicas}{"\t"}{.status.replicas}{"\n"}{end}' 2>/dev/null || true)
fi
while IFS=$'\t' read -r ns ready replicas; do
[[ -z "$ns" ]] && continue
ready=${ready:-0}
replicas=${replicas:-0}
if (( ready >= 1 && replicas >= 1 )); then
return 0
fi
done <<< "$deployment_rows"
fi
now=$(date +%s)
if (( now - start_time >= PLUGIN_READY_TIMEOUT )); then
echo "ERROR: Timed out waiting for CNPG plugin '$BARMAN_PLUGIN_NAME' to become available." >&2
return 1
fi
echo "Waiting for CNPG plugin '$BARMAN_PLUGIN_NAME' to become available ..."
sleep 5
done
}
has_successful_base_backup() {
local rows name phase method backup_type phase_lc method_lc backup_type_lc
rows=$(kubectl get backup -n "$NAMESPACE" -o jsonpath="{range .items[?(@.spec.cluster.name=='$CNPG_CLUSTER_NAME')]}{.metadata.name}{\"\t\"}{.status.phase}{\"\t\"}{.spec.method}{\"\t\"}{.spec.pluginConfiguration.parameters.backupType}{\"\n\"}{end}" 2>/dev/null || true)
if [[ -z "$rows" ]]; then
return 1
fi
while IFS=$'\t' read -r name phase method backup_type; do
[[ -z "$name" ]] && continue
phase_lc=$(echo "${phase:-}" | tr '[:upper:]' '[:lower:]')
method_lc=$(echo "${method:-}" | tr '[:upper:]' '[:lower:]')
backup_type_lc=$(echo "${backup_type:-}" | tr '[:upper:]' '[:lower:]')
if [[ "$phase_lc" != "completed" && "$phase_lc" != "succeeded" ]]; then
continue
fi
if [[ "$method_lc" == "plugin" ]] && [[ "$backup_type_lc" == "incremental" || "$backup_type_lc" == "incr" ]]; then
continue
fi
return 0
done <<< "$rows"
return 1
}
wait_for_successful_base_backup() {
local start_time now elapsed
start_time=$(date +%s)
while true; do
if has_successful_base_backup; then
return 0
fi
now=$(date +%s)
elapsed=$((now - start_time))
if (( elapsed >= BACKUP_STATUS_TIMEOUT )); then
return 1
fi
echo "Waiting for a successful base backup ... (${elapsed}s/${BACKUP_STATUS_TIMEOUT}s)"
sleep "$BACKUP_STATUS_INTERVAL"
done
}
trigger_backup() {
local backup_type="${1:-full}"
local backup_name
@ -281,6 +363,14 @@ status() {
ensure_tools
echo "Backups for cluster '$CNPG_CLUSTER_NAME' in namespace '$NAMESPACE':"
kubectl get backup -n "$NAMESPACE" | grep "$CNPG_CLUSTER_NAME" || true
if has_successful_base_backup; then
echo "Backup status: OK (at least one successful base backup is available)."
return 0
fi
echo "ERROR: No successful base backup found for cluster '$CNPG_CLUSTER_NAME'." >&2
return 1
}
case "$ACTION" in
@ -291,10 +381,17 @@ case "$ACTION" in
ensure_garage_bucket_and_key
apply_barman_object_store
ensure_barman_plugin_config
wait_for_plugin_ready
remove_native_barman_config
if [[ "$RUN_FIRST_BACKUP" == "1" ]]; then
trigger_backup
fi
if ! wait_for_successful_base_backup; then
echo "ERROR: Timed out waiting for a successful base backup." >&2
status || true
exit 1
fi
status
;;
backup)
ensure_tools

View File

@ -162,15 +162,15 @@ run_barman_backup() {
return 1
fi
if [[ -x "$SCRIPT_DIR/init_prole-db-backup.sh" ]]; then
if [[ -x "$SCRIPT_DIR/init_cnpg_backup.sh" ]]; then
log "Running barman backup (Garage bucket: $GARAGE_BACKUP_BUCKET) ..."
NAMESPACE="$NAMESPACE" \
GARAGE_BACKUP_BUCKET="$GARAGE_BACKUP_BUCKET" \
GARAGE_BACKUP_SECRET_NAME="$GARAGE_BACKUP_SECRET_NAME" \
GARAGE_S3_ENDPOINT="$GARAGE_S3_ENDPOINT" \
RUN_FIRST_BACKUP=1 "$SCRIPT_DIR/init_prole-db-backup.sh" start
RUN_FIRST_BACKUP=1 "$SCRIPT_DIR/init_cnpg_backup.sh" start
else
err "init_prole-db-backup.sh not found; cannot run backup."
err "init_cnpg_backup.sh not found; cannot run backup."
return 1
fi

View File

@ -253,7 +253,7 @@ STATUS_SCRIPTS=(
"init_kong.sh"
"init_db_manager.sh"
"init_monitoring.sh"
"init_prole-db-backup.sh"
"init_cnpg_backup.sh"
"init_port_forwards.sh"
)

View File

@ -1 +1 @@
89
92

View File

@ -800,15 +800,15 @@ case "${CMD}" in
if [[ -x "${ROOT_DIR}/etc/init_cloudnative_pg.sh" ]]; then
"${ROOT_DIR}/etc/init_cloudnative_pg.sh" install-barman-plugin
fi
if [[ -x "${ROOT_DIR}/etc/init_prole-db-backup.sh" ]]; then
RUN_FIRST_BACKUP=0 "${ROOT_DIR}/etc/init_prole-db-backup.sh" start
if [[ -x "${ROOT_DIR}/etc/init_cnpg_backup.sh" ]]; then
RUN_FIRST_BACKUP=0 "${ROOT_DIR}/etc/init_cnpg_backup.sh" start
fi
log_msg "Running full backup..."
backup_before=$(kubectl -n "${NAMESPACE:-default}" get backup \
--sort-by=.metadata.creationTimestamp \
-o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null | tail -n 1 || true)
"${ROOT_DIR}/etc/init_prole-db-backup.sh" backup
"${ROOT_DIR}/etc/init_cnpg_backup.sh" backup
backup_name=""
for i in {1..30}; do
@ -893,8 +893,8 @@ case "${CMD}" in
if [[ -x "${ROOT_DIR}/etc/init_cloudnative_pg.sh" ]]; then
"${ROOT_DIR}/etc/init_cloudnative_pg.sh" install-barman-plugin
fi
if [[ -x "${ROOT_DIR}/etc/init_prole-db-backup.sh" ]]; then
RUN_FIRST_BACKUP=0 "${ROOT_DIR}/etc/init_prole-db-backup.sh" start
if [[ -x "${ROOT_DIR}/etc/init_cnpg_backup.sh" ]]; then
RUN_FIRST_BACKUP=0 "${ROOT_DIR}/etc/init_cnpg_backup.sh" start
fi
log_msg "Running ${backup_type} backup..."
@ -903,9 +903,9 @@ case "${CMD}" in
-o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null | tail -n 1 || true)
if [[ "$backup_type" == "full" ]]; then
"${ROOT_DIR}/etc/init_prole-db-backup.sh" backup full
"${ROOT_DIR}/etc/init_cnpg_backup.sh" backup full
else
"${ROOT_DIR}/etc/init_prole-db-backup.sh" backup incr
"${ROOT_DIR}/etc/init_cnpg_backup.sh" backup incr
fi
backup_name=""
@ -1008,7 +1008,7 @@ case "${CMD}" in
fi
# 4. Database backup infrastructure
run_init_script init_prole-db-backup.sh start
run_init_script init_cnpg_backup.sh start
# 5. Kong API Gateway
run_init_script init_kong.sh -n "${NAMESPACE:-prole-db}" start

View File

@ -109,7 +109,7 @@ _STATUS_SCRIPTS: list[str] = [
"init_postgrest.sh",
"init_db_manager.sh",
"init_monitoring.sh",
"init_prole-db-backup.sh",
"init_cnpg_backup.sh",
"init_port_forwards.sh",
]

View File

@ -15,10 +15,12 @@ FOREGROUND="false"
FORCE="true"
PROLE_CFG_PATH=""
HELM_TEMPLATE_ONLY="false"
PREFETCH_IMAGES_ONLY="false"
SKIP_PREFETCH="false"
usage() {
cat <<'USAGE'
Prole::Supabase
supabase/deploy.sh
Launches the full Supabase open-source stack locally, following the
README/DEVELOPERS guidance (Docker Compose). This is a multi-service deployment
@ -30,6 +32,8 @@ Usage:
Options:
--mode <local|k3d|k8s> Deployment mode ('local' for Docker Compose, 'k3d' for local k3d, 'k8s' for generic k8s/k3s; default: k3d)
--helm-template-only Render Helm manifests to supabase/k8s without applying (for pipelines)
--prefetch-images-only Discover/prepare/load/import Supabase images only, then exit (k3d mode)
--skip-prefetch Skip image prefetch during deploy (for preloaded flows)
--with-dev-helpers Include docker/dev/docker-compose.dev.yml
--foreground Run docker compose in the foreground (default: detached, local mode only)
-f, --force Reset the Supabase namespace before applying manifests (k3d only; default)
@ -270,6 +274,14 @@ parse_args() {
HELM_TEMPLATE_ONLY="true"
shift
;;
--prefetch-images-only)
PREFETCH_IMAGES_ONLY="true"
shift
;;
--skip-prefetch)
SKIP_PREFETCH="true"
shift
;;
--foreground)
FOREGROUND="true"
shift
@ -448,10 +460,58 @@ load_image_for_platform() {
fi
log "Loading $img ($platform) from $tar_path"
docker load -i "$tar_path" >/dev/null
docker load -i "$tar_path" 2>&1 | sed 's/^/ /'
docker tag "$platform_tag" "$img"
}
# Returns 0 if the image is already present in k3d's containerd store, 1 otherwise.
image_in_k3d() {
local img="$1"
local cluster="${K3D_CLUSTER_NAME:-}"
local node="k3d-${cluster}-server-0"
if ! docker inspect "$node" >/dev/null 2>&1; then
return 1
fi
docker exec "$node" crictl images --no-trunc -o json 2>/dev/null \
| python3 - "$img" <<'PY'
import json, sys
target = sys.argv[1]
try:
data = json.load(sys.stdin)
except Exception:
sys.exit(1)
for entry in data.get('images', []):
for tag in entry.get('repoTags', []):
if tag == target:
sys.exit(0)
sys.exit(1)
PY
}
# Build the list of images already present in k3d's containerd store.
list_k3d_images() {
local cluster="${K3D_CLUSTER_NAME:-}"
local node="k3d-${cluster}-server-0"
if ! docker inspect "$node" >/dev/null 2>&1; then
return 0
fi
docker exec "$node" crictl images --no-trunc -o json 2>/dev/null \
| python3 - <<'PY'
import json, sys
try:
data = json.load(sys.stdin)
except Exception:
sys.exit(0)
for entry in data.get('images', []):
for tag in entry.get('repoTags', []):
print(tag)
PY
}
write_artifact_manifest() {
local images="$1"
local list_path="${DOCKER_IMPORT_DIR}/supabase-images.txt"
@ -510,12 +570,44 @@ prefetch_k3d_images() {
load_image_for_platform "$img" "$deploy_platform"
done
# shellcheck disable=SC2086
if [[ -n "${K3D_CLUSTER_NAME:-}" ]]; then
k3d image import $images -c "$K3D_CLUSTER_NAME"
else
k3d image import $images
# Build a list of images already present in k3d to avoid re-importing them.
log "Checking k3d registry for already-imported images..."
local k3d_existing
k3d_existing="$(list_k3d_images || true)"
local -a import_delta=()
local -a skipped=()
for img in $images; do
if printf '%s\n' "$k3d_existing" | grep -qxF "$img"; then
skipped+=("$img")
else
import_delta+=("$img")
fi
done
if [[ ${#skipped[@]} -gt 0 ]]; then
log "Already in k3d registry (skipping ${#skipped[@]} image(s)):"
printf " [skip] %s\n" "${skipped[@]}"
fi
if [[ ${#import_delta[@]} -eq 0 ]]; then
log "All images already present in k3d registry; nothing to import."
return 0
fi
log "Importing ${#import_delta[@]} image(s) into k3d cluster '${K3D_CLUSTER_NAME:-}' (delta only)..."
local idx=0
for img in "${import_delta[@]}"; do
idx=$(( idx + 1 ))
log " [${idx}/${#import_delta[@]}] Importing $img into k3d..."
if [[ -n "${K3D_CLUSTER_NAME:-}" ]]; then
k3d image import "$img" -c "$K3D_CLUSTER_NAME"
else
k3d image import "$img"
fi
log " [${idx}/${#import_delta[@]}] Done: $img"
done
log "k3d image import complete (${#import_delta[@]} imported, ${#skipped[@]} already present)."
}
resolve_prole_db_namespace() {
@ -1123,7 +1215,11 @@ run_k3d() {
export SUPABASE_IMAGE_PLATFORM
SUPABASE_IMAGE_PLATFORM="$(normalize_platform "${SUPABASE_IMAGE_PLATFORM:-}")"
prefetch_k3d_images
if [[ "$SKIP_PREFETCH" != "true" ]]; then
prefetch_k3d_images
else
log "Skipping Supabase image prefetch (requested)."
fi
generate_k8s_manifests
setup_prole_db_for_supabase
@ -1139,6 +1235,32 @@ run_k3d() {
log "Access Studio via Kong proxy (check ingress/service in 'supabase' namespace)."
}
run_prefetch_images_only() {
ensure_repo
case "$MODE" in
k3d)
require_cmd kubectl
ensure_k3d
ensure_k3d_cluster
kubectl cluster-info >/dev/null 2>&1 || die "Kubernetes cluster not reachable"
export SUPABASE_HOME="$SUPABASE_DIR"
if [[ "$USE_DEV_HELPERS" == "true" ]]; then
export SUPABASE_USE_DEV_COMPOSE=1
fi
export SUPABASE_IMAGE_PLATFORM
SUPABASE_IMAGE_PLATFORM="$(normalize_platform "${SUPABASE_IMAGE_PLATFORM:-}")"
prefetch_k3d_images
log "Supabase image prefetch complete."
;;
*)
die "--prefetch-images-only currently supports only --mode k3d"
;;
esac
}
run_k8s() {
# Helm-first path (preferred)
if run_helm; then
@ -1191,6 +1313,11 @@ main() {
exit 1
fi
if [[ "$PREFETCH_IMAGES_ONLY" == "true" ]]; then
run_prefetch_images_only
return 0
fi
case "$MODE" in
local)
run_local

View File

@ -29,13 +29,13 @@
},
"secret": {
"db": {
"password": "${OPENBAO:kv/prole/${NAMESPACE}/db#password}",
"database": "${NAMESPACE}"
"password": "${OPENBAO:kv/prole/prole-db001/db#password}",
"database": "postgres"
},
"jwt": {
"secret": "4e53e1ecb98eb32ee83b890bea79e678e952c6ae237edf3f65f37bf6dd2262eb",
"anonKey": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InByb2xlLXN1cGFiYXNlIiwiaWF0IjoxNzcyMDk5NDM2LCJleHAiOjIwODc0NTk0MzZ9.NM4QHNzdWWSyvvBm-xiHdFJm8T12cROyGhvrx2eMaPk",
"serviceKey": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoicHJvbGUtc3VwYWJhc2UiLCJpYXQiOjE3NzIwOTk0MzYsImV4cCI6MjA4NzQ1OTQzNn0.53NMhvhhEmCQRJQ_UW5Cc6TRMI3X0Zrf4gmiMNjU2As"
"anonKey": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InByb2xlLXN1cGFiYXNlIiwiaWF0IjoxNzcyNDM0NjM4LCJleHAiOjIwODc3OTQ2Mzh9.Wg05U6ioVntunXskmJs3DXMc1i9-O7UBts2cnBwPCoI",
"serviceKey": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoicHJvbGUtc3VwYWJhc2UiLCJpYXQiOjE3NzI0MzQ2MzgsImV4cCI6MjA4Nzc5NDYzOH0.xZ179fB412UFdgDxc8XUv2EQt-L1nGyhocll09u2u38"
}
},
"ingress": {

View File

@ -1,10 +1,10 @@
#!/usr/bin/env bash
# Unit test for etc/init_prole-db-backup.sh
# Unit test for etc/init_cnpg_backup.sh
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
PROLE_HOME=$(cd "$SCRIPT_DIR/../.." && pwd)
ETC_DIR="$PROLE_HOME/etc"
SCRIPT_UNDER_TEST="$ETC_DIR/init_prole-db-backup.sh"
SCRIPT_UNDER_TEST="$ETC_DIR/init_cnpg_backup.sh"
TMP_DIR=$(mktemp -d)
trap 'rm -rf "$TMP_DIR"' EXIT
@ -48,8 +48,8 @@ export NAMESPACE="test-ns"
export PROLE_PASSWD="test-password"
# Run the script
if [[ "init_prole-db-backup.sh" == *.py ]]; then
if [[ "init_prole-db-backup.sh" == "render_manifest.py" ]]; then
if [[ "init_cnpg_backup.sh" == *.py ]]; then
if [[ "init_cnpg_backup.sh" == "render_manifest.py" ]]; then
echo "apiVersion: v1" > "$TMP_DIR/test.yaml"
python3 "$SCRIPT_UNDER_TEST" "$TMP_DIR/test.yaml" > "$TMP_DIR/stdout" 2> "$TMP_DIR/stderr"
else

View File

@ -324,6 +324,27 @@ class TestProleController:
# Will fail since the script doesn't exist
assert rc != 0 or rc is None
def test_silent_installer_runs_supabase_preload_before_deploy(self, tmp_path):
c = ProleController(tmp_path)
installer = ProleSilentInstaller(c)
with mock.patch.object(installer, "_load_inputs_from_cfg", return_value={}), \
mock.patch.object(installer, "_write_cfg"), \
mock.patch.object(installer, "_perform_cluster_reset"), \
mock.patch.object(installer, "_close_log_file"):
captured = {}
def fake_run_milestones(milestones, progress_callback=None):
captured["ids"] = [m.id for m in milestones]
c.run_milestones = fake_run_milestones
rc = installer.run()
assert rc == 0
assert "supabase_images_preload" in captured["ids"]
assert "supabase" in captured["ids"]
assert captured["ids"].index("supabase_images_preload") < captured["ids"].index("supabase")
# ===== Milestone concrete subclasses (from core/milestones.py) =====

View File

@ -18,6 +18,7 @@ from installer.core.milestones import (
DockerBuildMilestone,
GitOpsMilestone,
KerberosMilestone,
SupabaseImagePreloadMilestone,
SupabaseMilestone,
DependenciesMilestone,
InitializationScriptsMilestone,
@ -350,9 +351,89 @@ def test_kerberos_enabled_no_mode():
# ---------------------------------------------------------------------------
# SupabaseMilestone
# Supabase milestones
# ---------------------------------------------------------------------------
def test_supabase_image_preload_disabled():
state = _make_state(**{"init_cluster.supabase_enabled": "False"})
milestone = SupabaseImagePreloadMilestone()
milestone.execute(state)
assert state.config_data["Supabase"]["IMAGES_STATUS"] == "Skipped"
def test_supabase_image_preload_success(tmp_path):
state = _make_state(**{
"init_cluster.supabase_enabled": "True",
"init_cluster.cluster_env": "dev",
})
state.controller.project_root = tmp_path
supa_dir = tmp_path / "supabase"
supa_dir.mkdir()
(supa_dir / "deploy.sh").write_text("#!/bin/bash\nexit 0\n")
mock_result = MagicMock()
mock_result.returncode = 0
captured_cmd = []
def capture_run(cmd, **kwargs):
captured_cmd.extend(cmd)
return mock_result
milestone = SupabaseImagePreloadMilestone()
with patch.object(milestone, "_get_script_env", return_value={}), \
patch("installer.core.milestones.subprocess.run", side_effect=capture_run), \
patch("installer.core.env._normalize_cluster_env", return_value="dev"), \
patch("installer.core.env._parse_bool", return_value=False):
milestone.execute(state)
assert state.config_data["Supabase"]["IMAGES_STATUS"] == "Prepared"
assert state.inputs["supabase.images_preloaded"] == "True"
assert "--prefetch-images-only" in captured_cmd
assert "k3d" in captured_cmd
def test_supabase_image_preload_failure_raises(tmp_path):
state = _make_state(**{
"init_cluster.supabase_enabled": "True",
"init_cluster.cluster_env": "dev",
})
state.controller.project_root = tmp_path
supa_dir = tmp_path / "supabase"
supa_dir.mkdir()
(supa_dir / "deploy.sh").write_text("#!/bin/bash\nexit 1\n")
mock_result = MagicMock()
mock_result.returncode = 1
milestone = SupabaseImagePreloadMilestone()
with patch.object(milestone, "_get_script_env", return_value={}), \
patch("installer.core.milestones.subprocess.run", return_value=mock_result), \
patch("installer.core.env._normalize_cluster_env", return_value="dev"), \
patch("installer.core.env._parse_bool", return_value=False):
with pytest.raises(Exception, match="[Pp]reload"):
milestone.execute(state)
assert state.config_data["Supabase"]["IMAGES_STATUS"] == "Attempted"
def test_supabase_image_preload_skips_for_k8s_mode(tmp_path):
state = _make_state(**{
"init_cluster.supabase_enabled": "True",
"init_cluster.cluster_env": "prod",
})
state.controller.project_root = tmp_path
supa_dir = tmp_path / "supabase"
supa_dir.mkdir()
(supa_dir / "deploy.sh").write_text("#!/bin/bash\nexit 0\n")
milestone = SupabaseImagePreloadMilestone()
with patch.object(milestone, "_get_script_env", return_value={}), \
patch("installer.core.milestones.subprocess.run") as mock_run, \
patch("installer.core.env._normalize_cluster_env", return_value="prod"):
milestone.execute(state)
assert state.config_data["Supabase"]["IMAGES_STATUS"] == "Skipped"
mock_run.assert_not_called()
def test_supabase_disabled():
state = _make_state(**{"init_cluster.supabase_enabled": "False"})
milestone = SupabaseMilestone()
@ -447,6 +528,33 @@ def test_supabase_enabled_with_progress(tmp_path):
progress.assert_called()
def test_supabase_deploy_skips_prefetch_when_preloaded(tmp_path):
state = _make_state(**{
"init_cluster.supabase_enabled": "True",
"init_cluster.cluster_env": "dev",
"supabase.images_preloaded": "True",
})
state.controller.project_root = tmp_path
supa_dir = tmp_path / "supabase"
supa_dir.mkdir()
(supa_dir / "deploy.sh").write_text("#!/bin/bash\nexit 0\n")
mock_result = MagicMock()
mock_result.returncode = 0
captured_cmd = []
def capture_run(cmd, **kwargs):
captured_cmd.extend(cmd)
return mock_result
milestone = SupabaseMilestone()
with patch.object(milestone, "_get_script_env", return_value={}), \
patch("installer.core.milestones.subprocess.run", side_effect=capture_run), \
patch("installer.core.env._normalize_cluster_env", return_value="dev"):
milestone.execute(state)
assert "--skip-prefetch" in captured_cmd
# ---------------------------------------------------------------------------
# DependenciesMilestone — error paths not covered by existing tests
# ---------------------------------------------------------------------------