Configure Silent Install Test with unique logging and shared run configuration

- Updated tests/silent_install_test.sh to support unique logging via SILENT_INSTALL_LOG=true

- Created shared IntelliJ Run Configuration '.idea/runConfigurations/Silent_Install_Test.xml'

- Updated various init scripts, port mappings, and installer logic

- Added supabase.sh and init_monitoring.sh
This commit is contained in:
chrisfu 2026-02-02 16:13:15 -08:00
parent 5ab6381982
commit 12d00c468a
14 changed files with 974 additions and 268 deletions

View File

@ -0,0 +1,19 @@
<component name="RunManager">
<configuration name="Silent Install Test" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/tests/silent_install_test.sh" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/bin/bash" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="true" />
<envs>
<env name="SILENT_INSTALL_LOG" value="true" />
</envs>
<method v="2" />
</configuration>
</component>

View File

@ -2,9 +2,8 @@
<portMappings>
<mapping id="dashboard" namespace="kubernetes-dashboard" target="svc/kubernetes-dashboard-kong-proxy" address="127.0.0.1" hostPort="8443" servicePort="443" protocol="TCP" description="Kubernetes Dashboard (https://127.0.0.1:8443)"/>
<mapping id="prometheus" namespace="${NAMESPACE}" target="svc/prometheus-community-kube-prometheus" address="127.0.0.1" hostPort="9090" servicePort="9090" protocol="TCP" description="Prometheus UI (http://127.0.0.1:9090)"/>
<mapping id="grafana" namespace="${NAMESPACE}" target="svc/prometheus-community-grafana" address="127.0.0.1" 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="prole-db" namespace="${NAMESPACE}" target="svc/prole-db-rw" address="0.0.0.0" hostPort="15432" servicePort="5432" protocol="TCP" description="Prole DB (alternate port when Supabase is enabled)"/>
<mapping id="supabase-db" namespace="supabase" target="svc/db" address="0.0.0.0" hostPort="5432" servicePort="5432" protocol="TCP" description="Supabase PostgreSQL (127.0.0.1:5432)"/>
<mapping id="openbao" namespace="${NAMESPACE}" target="svc/openbao" address="127.0.0.1" hostPort="18200" servicePort="8200" protocol="TCP" description="OpenBao (http://127.0.0.1:18200)"/>
</portMappings>

2
env.sh
View File

@ -8,7 +8,7 @@ export PROLE_CONF="/Users/chrisfu/dev/prole/conf"
export PROLE_DATA="/Users/chrisfu/dev/prole/data"
export PROLE_LOGS="/Users/chrisfu/dev/prole/logs"
export PROLE_SERVICE="/Users/chrisfu/dev/prole/etc"
export NAMESPACE="prole-chrisfu-abacababa"
export NAMESPACE="prole-chrisfu-abracadabra"
# Ensure PATH works for GUI-launched shells (Docker, etc.)
_prole_add_path() { case ":${PATH}:" in *":$1:"*) ;; *) PATH="$1:${PATH:-}" ;; esac; }

View File

@ -14,7 +14,7 @@ set -euo pipefail
# ./init_cloudnative_pg.sh update|reload # re-apply/patch
#
# Requirements:
# - init_openbao.sh has been run (OpenBao running in k8s)
# - init_openbao.sh has been run (OpenBao running as a local container)
# - $PROLE_HOME/env.sh or $HOME/.prole/env.sh defining PROLE_SERVICE
# - Optional: CNPG_MANIFEST_OVERRIDE to apply a recovery manifest instead of kustomize
@ -310,11 +310,6 @@ initialize() {
ensure_cnpg_operator
echo "Using namespace: $NAMESPACE"
# Ensure port-forward is running for OpenBao (dependency)
echo "Ensuring port-forward for OpenBao is active ..."
"$SCRIPT_DIR/init_port_forwards.sh" restart openbao &
sleep 2
fetch_admin_keys_and_db_pass_from_bao_or_local
apply_cnpg_admin_secret
@ -341,7 +336,8 @@ initialize() {
# Ensure port-forward is running for Postgres (local access)
echo "Ensuring port-forward for Postgres is active ..."
"$SCRIPT_DIR/init_port_forwards.sh" restart postgres &
"$SCRIPT_DIR/init_port_forwards.sh" stop postgres || true
"$SCRIPT_DIR/init_port_forwards.sh" start postgres &
sleep 2
echo "Initialization complete for CNPG + Kerberos + cert artifacts."

View File

@ -215,14 +215,17 @@ init_layout() {
assign_count=$((assign_count + 1))
done
# Re-fetch layout and apply staged changes using the current layout version
local layout_after version apply_ok
layout_after=$(kubectl exec -n "$NAMESPACE" "$pod" -- /garage layout show 2>/dev/null || true)
version=$(echo "$layout_after" | awk -F: '/[Vv]ersion/ {gsub(/ /,"",$2); print $2; exit}' || true)
# Re-fetch layout and apply staged changes using the NEXT layout version
local layout_after version next_version apply_ok
layout_after=$(kubectl exec -n "$NAMESPACE" "$pod" -- /garage layout show 2>/dev/null | grep -v "INFO" || true)
# Extract "Current cluster layout version: X"
version=$(echo "$layout_after" | awk -F: '/Current cluster layout version/ {gsub(/[[:space:]]/,"",$2); print $2; exit}' || true)
if echo "$layout_after" | grep -qi "staged"; then
if [[ -n "$version" ]]; then
echo "Applying Garage layout (version $version) ..."
if kubectl exec -n "$NAMESPACE" "$pod" -- /garage layout apply --version "$version"; then
next_version=$((version + 1))
echo "Applying Garage layout (current version $version, next version $next_version) ..."
if kubectl exec -n "$NAMESPACE" "$pod" -- /garage layout apply --version "$next_version"; then
apply_ok=1
else
apply_ok=0

View File

@ -95,9 +95,9 @@ ensure_configmap() {
if kubectl -n "$NAMESPACE" get configmap prole-krb5-conf >/dev/null 2>&1; then
return
fi
echo "ConfigMap prole-krb5-conf not found. Running init_openbao.sh update..."
if [[ -x "$SCRIPT_DIR/init_openbao.sh" ]]; then
KRB5_REALM="$KRB5_REALM" KRB5_KDC="$KRB5_KDC" "$SCRIPT_DIR/init_openbao.sh" update || true
echo "ConfigMap prole-krb5-conf not found. Running init_kerberos.sh update..."
if [[ -x "$SCRIPT_DIR/init_kerberos.sh" ]]; then
KRB5_REALM="$KRB5_REALM" KRB5_KDC="$KRB5_KDC" "$SCRIPT_DIR/init_kerberos.sh" update || true
fi
}

158
etc/init_monitoring.sh Executable file
View File

@ -0,0 +1,158 @@
#!/usr/bin/env bash
set -euo pipefail
# init_monitoring.sh
# Purpose:
# - Configure k3d environment for monitoring (Prometheus and Grafana)
# - Setup kube-prometheus-stack and CNPG prometheus rules
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"
if [[ -z "${PROLE_SERVICE:-}" ]]; then
echo "ERROR: PROLE_SERVICE is not defined. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2
exit 1
fi
log() {
echo "==> $*"
}
err() {
echo "ERROR: $*" >&2
}
ensure_tools() {
for t in helm kubectl; do
command -v "$t" >/dev/null || { err "Missing required tool: $t"; exit 1; }
done
}
ensure_namespace() {
local ns="$1"
if ! kubectl get namespace "$ns" >/dev/null 2>&1; then
log "Creating namespace '$ns' ..."
kubectl create namespace "$ns" >/dev/null 2>&1 || true
fi
}
cleanup_grafana_rbac_conflicts() {
local release="grafana-prole"
local ns="$NAMESPACE"
local cr="${release}-clusterrole"
local crb="${release}-clusterrolebinding"
local rel_ns rel_name
if kubectl get clusterrole "$cr" >/dev/null 2>&1; then
rel_ns=$(kubectl get clusterrole "$cr" -o jsonpath='{.metadata.annotations.meta\.helm\.sh/release-namespace}' 2>/dev/null || true)
rel_name=$(kubectl get clusterrole "$cr" -o jsonpath='{.metadata.annotations.meta\.helm\.sh/release-name}' 2>/dev/null || true)
if [[ -n "$rel_ns" && "$rel_ns" != "$ns" ]]; then
log "Detected existing ClusterRole '$cr' owned by release '${rel_name:-unknown}' in namespace '$rel_ns'."
if [[ -n "$rel_name" ]] && helm status "$rel_name" -n "$rel_ns" >/dev/null 2>&1; then
log "Uninstalling old Grafana release '$rel_name' from '$rel_ns' ..."
helm uninstall "$rel_name" -n "$rel_ns" || true
fi
if kubectl get clusterrole "$cr" >/dev/null 2>&1; then
log "Deleting orphaned ClusterRole '$cr' ..."
kubectl delete clusterrole "$cr" || true
fi
fi
fi
if kubectl get clusterrolebinding "$crb" >/dev/null 2>&1; then
rel_ns=$(kubectl get clusterrolebinding "$crb" -o jsonpath='{.metadata.annotations.meta\.helm\.sh/release-namespace}' 2>/dev/null || true)
rel_name=$(kubectl get clusterrolebinding "$crb" -o jsonpath='{.metadata.annotations.meta\.helm\.sh/release-name}' 2>/dev/null || true)
if [[ -n "$rel_ns" && "$rel_ns" != "$ns" ]]; then
log "Detected existing ClusterRoleBinding '$crb' owned by release '${rel_name:-unknown}' in namespace '$rel_ns'."
if [[ -n "$rel_name" ]] && helm status "$rel_name" -n "$rel_ns" >/dev/null 2>&1; then
log "Uninstalling old Grafana release '$rel_name' from '$rel_ns' ..."
helm uninstall "$rel_name" -n "$rel_ns" || true
fi
if kubectl get clusterrolebinding "$crb" >/dev/null 2>&1; then
log "Deleting orphaned ClusterRoleBinding '$crb' ..."
kubectl delete clusterrolebinding "$crb" || true
fi
fi
fi
}
install_monitoring() {
ensure_tools
local monitoring_ns="default"
ensure_namespace "$monitoring_ns"
ensure_namespace "$NAMESPACE"
log "Installing kube-prometheus-stack in namespace '$monitoring_ns' ..."
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts || true
helm repo update prometheus-community || true
# Install Prometheus stack in default namespace, but disable Grafana there
# Also set grafana.enabled=false explicitly to avoid conflicts if it was previously enabled.
# Use --force-conflicts with Server-Side Apply (SSA) to handle webhook conflicts.
# SSA is more robust for managing shared resources like webhooks.
helm upgrade --install \
--namespace "$monitoring_ns" \
--set grafana.enabled=false \
--force-conflicts \
--server-side=true \
-f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/main/docs/src/samples/monitoring/kube-stack-config.yaml \
prometheus-community prometheus-community/kube-prometheus-stack
log "Installing Grafana in namespace '$NAMESPACE' ..."
# We use the same chart but only for Grafana, or we could use the standalone grafana chart.
# Using the same chart ensures we can use the same config if needed, but we must avoid ClusterRole conflicts.
# Actually, standalone grafana chart is cleaner if we only want Grafana.
helm repo add grafana https://grafana.github.io/helm-charts || true
helm repo update grafana || true
local prometheus_svc="http://prometheus-community-kube-prometheus.$monitoring_ns.svc.cluster.local:9090"
cleanup_grafana_rbac_conflicts
# Note: kube-prometheus-stack may have already been installed with Grafana enabled in 'default'.
# If we want to move Grafana to $NAMESPACE, we install it there.
# We use the grafana/grafana chart for the per-namespace instance.
# Use --force-conflicts with Server-Side Apply (SSA) to handle potential conflicts during upgrade.
helm upgrade --install \
--namespace "$NAMESPACE" \
--force-conflicts \
--server-side=true \
--set "rbac.namespaced=true" \
--set "datasources.datasources\.yaml.apiVersion=1" \
--set "datasources.datasources\.yaml.datasources[0].name=Prometheus" \
--set "datasources.datasources\.yaml.datasources[0].type=prometheus" \
--set "datasources.datasources\.yaml.datasources[0].url=$prometheus_svc" \
--set "datasources.datasources\.yaml.datasources[0].access=proxy" \
--set "datasources.datasources\.yaml.datasources[0].isDefault=true" \
--set "adminPassword=admin" \
grafana-prole grafana/grafana
log "Applying CNPG prometheus rules in namespace '$NAMESPACE'..."
kubectl apply --namespace "$NAMESPACE" -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/main/docs/src/samples/monitoring/prometheusrule.yaml
log "Retrieving Grafana admin password from namespace '$NAMESPACE'..."
# Secret name is 'grafana-prole' from the helm release name
GRAFANA_PASSWORD=$(kubectl --namespace "$NAMESPACE" get secrets grafana-prole -o jsonpath="{.data.admin-password}" 2>/dev/null | base64 -d || true)
if [[ -n "$GRAFANA_PASSWORD" ]]; then
log "Grafana installation password: $GRAFANA_PASSWORD"
# We will save this to prole.cfg via the installer, but also output it here for logs
echo "GRAFANA_ADMIN_PASSWORD=$GRAFANA_PASSWORD"
else
err "Failed to retrieve Grafana admin password."
fi
}
case "${1:-}" in
initialize)
install_monitoring
;;
*)
echo "Usage: $0 initialize"
exit 1
;;
esac

View File

@ -4,9 +4,9 @@ set -euo pipefail
# init_openbao.sh
# Purpose:
# - Deploy OpenBao to Kubernetes (dev mode) and store admin key pair
# Generate and apply a Kerberos krb5.conf ConfigMap for an external realm
# - Local Docker helpers for OpenBao (optional)
# - Run OpenBao as a local Docker container (dev mode) and store admin key pair
# - Optionally generate Kerberos krb5.conf ConfigMap for external realm (legacy k8s flow)
# - Store secrets in OpenBao kv (admin keys, user keys, db password, optional TDE keys)
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
@ -49,11 +49,15 @@ admin_key_pub="$SECRETS_DIR/admin_ed25519.pub"
root_token_file="$SECRETS_DIR/openbao-root-token"
ensure_tools() {
for t in kubectl curl openssl base64; do
for t in curl openssl base64 jq; do
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
done
}
ensure_k8s_tools() {
command -v kubectl >/dev/null || { echo "Missing required tool: kubectl" >&2; exit 1; }
}
ensure_namespace() {
if ! kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then
echo "Creating namespace '$NAMESPACE' ..."
@ -137,6 +141,31 @@ EOF
fi
}
bao_service_url() {
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
echo "$PROLE_OPENBAO_URL"
return 0
fi
echo "http://127.0.0.1:18200"
}
wait_for_openbao_http() {
local svc
svc="$(bao_service_url)"
local max_retries=${1:-20}
local count=0
while ! curl -sS "$svc/v1/sys/health" >/dev/null 2>&1; do
if [[ $count -ge $max_retries ]]; then
echo "ERROR: OpenBao not reachable at $svc after waiting." >&2
return 1
fi
echo "Waiting for OpenBao to be ready... ($count/$max_retries)"
sleep 2
count=$((count + 1))
done
return 0
}
detect_openbao_root_token() {
local token=""
local file
@ -241,7 +270,8 @@ init_openbao_kv_and_store_admin_key() {
local token
token=$(cat "$root_token_file")
# Enable kv (if not already) and write keys
local svc="http://127.0.0.1:18200"
local svc
svc="$(bao_service_url)"
echo "Configuring KV at $svc ..."
# Check if kv/ is already mounted
@ -330,6 +360,55 @@ init_openbao_kv_and_store_admin_key() {
echo "Stored database password in OpenBao kv/prole/db."
fi
# Store TDE keypair when at-rest encryption is enabled
local tde_enabled="${AT_REST_ENCRYPTION_ENABLED:-}"
case "$tde_enabled" in
1|true|TRUE|True|yes|YES|on|ON)
local tde_priv="$SECRETS_DIR/tde_ed25519.key"
local tde_pub="$SECRETS_DIR/tde_ed25519.pub"
if [[ ! -f "$tde_priv" || ! -f "$tde_pub" ]]; then
echo "Generating TDE keypair in $SECRETS_DIR ..."
if ! openssl genpkey -algorithm ED25519 -out "$tde_priv" 2>/dev/null; then
echo "ED25519 not supported by openssl, falling back to RSA 4096..."
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out "$tde_priv"
fi
openssl pkey -in "$tde_priv" -pubout -out "$tde_pub"
chmod 0600 "$tde_priv"
fi
local tde_priv_b64 tde_pub_b64
tde_priv_b64=$(base64 <"$tde_priv" | tr -d '\n')
tde_pub_b64=$(base64 <"$tde_pub" | tr -d '\n')
local tde_existing
tde_existing=$(curl -sS -H "X-Vault-Token: $token" "$svc/v1/kv/data/prole/tde" 2>/dev/null || true)
if [[ -n "$tde_existing" ]]; then
local ex_priv ex_pub
ex_priv=$(echo "$tde_existing" | grep -o '"tde_private_key_b64":"[^"]*' | cut -d'"' -f4 || true)
ex_pub=$(echo "$tde_existing" | grep -o '"tde_public_key_b64":"[^"]*' | cut -d'"' -f4 || true)
if [[ "$ex_priv" == "$tde_priv_b64" && "$ex_pub" == "$tde_pub_b64" ]]; then
echo "TDE keypair in OpenBao kv/prole/tde is already up to date."
else
echo "Writing TDE keypair to kv/prole/tde ..."
local tde_json
tde_json=$(echo "$tde_existing" | jq -c ".data.data")
local new_tde_json
new_tde_json=$(echo "$tde_json" | jq -c ". + {\"tde_private_key_b64\":\"$tde_priv_b64\",\"tde_public_key_b64\":\"$tde_pub_b64\"}")
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/tde" \
-d "{\"data\":$new_tde_json}" >/dev/null
echo "Stored TDE keypair in OpenBao kv/prole/tde."
fi
else
echo "Writing TDE keypair to kv/prole/tde ..."
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/tde" \
-d "{\"data\":{\"tde_private_key_b64\":\"$tde_priv_b64\",\"tde_public_key_b64\":\"$tde_pub_b64\"}}" >/dev/null
echo "Stored TDE keypair in OpenBao kv/prole/tde."
fi
;;
esac
local krb_user
krb_user="${KRB5_USER:-${KRB5_USERNAME:-}}"
if [[ -n "${KRB5_PASSWORD:-}" && -n "${KRB5_REALM:-}" && -n "$krb_user" ]]; then
@ -346,7 +425,6 @@ init_openbao_kv_and_store_admin_key() {
docker_start() {
echo "Starting local Docker container for OpenBao ..."
ensure_docker
docker rm -f "$OPENBAO_NAME" >/dev/null 2>&1 || true
# OpenBao dev
local token
token=$(cat "$root_token_file" 2>/dev/null || true)
@ -355,7 +433,29 @@ docker_start() {
printf "%s" "$token" >"$root_token_file"
chmod 0600 "$root_token_file"
fi
docker run -d --name "$OPENBAO_NAME" -p 18200:8200 \
local running existing host_ip
running=$(docker ps --filter "name=^/${OPENBAO_NAME}$" --format '{{.Names}}' | tr -d '\n')
if [[ "$running" == "$OPENBAO_NAME" ]]; then
echo "OpenBao container already running: $OPENBAO_NAME"
return 0
fi
existing=$(docker ps -a --filter "name=^/${OPENBAO_NAME}$" --format '{{.Names}}' | tr -d '\n')
if [[ "$existing" == "$OPENBAO_NAME" ]]; then
host_ip=$(docker inspect -f '{{(index (index .HostConfig.PortBindings "8200/tcp") 0).HostIp}}' "$OPENBAO_NAME" 2>/dev/null || echo "")
if [[ "$host_ip" == "127.0.0.1" || -z "$host_ip" ]]; then
echo "Starting existing OpenBao container: $OPENBAO_NAME"
docker start "$OPENBAO_NAME" >/dev/null
return 0
fi
echo "Existing OpenBao container is bound to $host_ip; recreating with 127.0.0.1..."
docker rm -f "$OPENBAO_NAME" >/dev/null 2>&1 || true
fi
docker run -d --name "$OPENBAO_NAME" \
--restart unless-stopped \
-p 127.0.0.1:18200:8200 \
"$OPENBAO_IMAGE" server -dev -dev-listen-address=0.0.0.0:8200 -dev-root-token-id="$token"
echo "Docker container started: $OPENBAO_NAME"
}
@ -392,7 +492,8 @@ cmd_status() {
ok=1
fi
# K8s resources
# K8s resources (legacy flow; best-effort only)
if command -v kubectl >/dev/null 2>&1; then
if kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" >/dev/null 2>&1; then
local ready desired
ready=$(kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0")
@ -406,7 +507,6 @@ cmd_status() {
ok=1
fi
elif kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" >/dev/null 2>&1; then
# Get readiness
local ready desired
ready=$(kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0")
desired=$(kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" -o jsonpath='{.status.replicas}' 2>/dev/null || echo "0")
@ -418,16 +518,10 @@ cmd_status() {
echo "[WARN] OpenBao Deployment not fully ready ($ready/$desired)"
ok=1
fi
else
echo "[MISSING] OpenBao Deployment or StatefulSet '$OPENBAO_NAME' in namespace '$NAMESPACE'"
ok=1
fi
if kubectl -n "$NAMESPACE" get configmap prole-krb5-conf >/dev/null 2>&1; then
echo "[OK] Kerberos ConfigMap 'prole-krb5-conf' present"
else
echo "[MISSING] Kerberos ConfigMap 'prole-krb5-conf' in namespace '$NAMESPACE'"
ok=1
fi
fi
# Docker (optional local mode)
@ -457,13 +551,7 @@ cmd_status() {
# OpenBao reachability and token validity (best-effort)
# Prefer explicit override, then localhost port-forward, then cluster DNS
local svc_url
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
svc_url="$PROLE_OPENBAO_URL"
elif curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then
svc_url="http://127.0.0.1:18200"
else
svc_url="http://$OPENBAO_NAME.$NAMESPACE.svc.cluster.local:8200"
fi
svc_url="$(bao_service_url)"
if curl -sS "$svc_url/v1/sys/health" >/dev/null 2>&1; then
echo "[OK] OpenBao service reachable"
if [[ -s "$root_token_file" ]]; then
@ -523,7 +611,7 @@ case "$ACTION" in
;;
initialize)
ensure_tools
ensure_namespace
ensure_docker
# Read password from stdin if provided
db_pass=""
if [[ ! -t 0 ]]; then
@ -531,52 +619,14 @@ case "$ACTION" in
fi
ensure_admin_keypair
generate_openbao_manifests
generate_kerberos_configmap
apply_k8s
apply_openbao_host_network_patch
if [[ -n "$db_pass" ]]; then
username=${PROLE_DB_USER:-"prole"}
echo "Checking/creating database user secret 'prole-db-user' for '$username' ..."
# Use apply to be idempotent
kubectl create secret generic prole-db-user -n "$NAMESPACE" \
--from-literal=username="$username" \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -n "$NAMESPACE" -f -
echo "Checking/creating database superuser secret 'prole-db-superuser' ..."
kubectl create secret generic prole-db-superuser -n "$NAMESPACE" \
--from-literal=username=postgres \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -n "$NAMESPACE" -f -
fi
wait_for_openbao
# Ensure port-forward is running for OpenBao
echo "Ensuring port-forward for OpenBao is active ..."
"$SCRIPT_DIR/init_port_forwards.sh" stop openbao || true
"$SCRIPT_DIR/init_port_forwards.sh" start openbao &
sleep 2
max_retries=15
count=0
while ! curl -sS http://127.0.0.1:18200/v1/sys/health >/dev/null 2>&1; do
if [[ $count -ge $max_retries ]]; then
echo "ERROR: OpenBao not reachable at http://127.0.0.1:18200 after waiting." >&2
echo "Please check logs: $SCRIPT_DIR/init_port_forwards.sh status openbao" >&2
exit 1
fi
echo "Waiting for OpenBao port-forward to be ready... ($count/$max_retries)"
sleep 2
count=$((count + 1))
done
docker_start
wait_for_openbao_http 20
init_openbao_kv_and_store_admin_key
echo "Initialization complete. Manifests in: $OPENBAO_MANIFEST_DIR"
echo "Initialization complete. OpenBao available at $(bao_service_url)"
;;
update|reload)
ensure_tools
ensure_k8s_tools
ensure_namespace
generate_openbao_manifests
generate_kerberos_configmap

View File

@ -82,7 +82,30 @@ parse_key_output() {
printf "%s\n%s" "$access_key" "$secret_key"
}
ensure_garage_ready() {
echo "Checking Garage readiness ..."
local i status_out
for i in {1..30}; do
if status_out=$(garage_exec status 2>/dev/null); then
# If layout is applied, DataAvail should eventually show something or at least the node should be healthy.
# A better check is 'layout show' to see if current layout version > 0
local layout_out version
layout_out=$(garage_exec layout show 2>/dev/null | grep -v "INFO" || true)
version=$(echo "$layout_out" | awk -F: '/Current cluster layout version/ {gsub(/[[:space:]]/,"",$2); print $2; exit}' || true)
if [[ -n "$version" && "$version" -gt 0 ]]; then
echo "Garage layout version $version is applied and ready."
return 0
fi
fi
echo "Waiting for Garage layout to be applied... ($i/30)"
sleep 5
done
echo "ERROR: Garage not ready (layout not applied) after 30 attempts." >&2
exit 1
}
ensure_garage_bucket_and_key() {
ensure_garage_ready
echo "Ensuring Garage bucket and access key for backups ..."
local key_info parsed access_key secret_key
if key_info=$(garage_exec key info --show-secret "$GARAGE_BACKUP_KEY_NAME" 2>/dev/null); then

View File

@ -29,7 +29,7 @@ else
fi
ensure_tools() {
for t in kubectl jq; do
for t in kubectl jq curl; do
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
done
}
@ -113,9 +113,10 @@ start() {
exit 1
fi
# 3. openbao is configured
if ! kubectl get statefulset openbao -n "$NAMESPACE" >/dev/null 2>&1 && ! kubectl get deployment openbao -n "$NAMESPACE" >/dev/null 2>&1; then
echo "ERROR: OpenBao is not deployed." >&2
# 3. openbao is configured (local container)
local openbao_url="${PROLE_OPENBAO_URL:-http://127.0.0.1:18200}"
if ! curl -sS "$openbao_url/v1/sys/health" >/dev/null 2>&1; then
echo "ERROR: OpenBao is not reachable at $openbao_url." >&2
exit 1
fi

View File

@ -80,6 +80,7 @@ DEFAULT_ACTION_FLAGS = {
'init_cnpg_deploy.run_deploy': True,
'init_cnpg_deploy.force_rollout': False,
'kerberos_config.test_connection': False,
'kerberos_config.init_authority': False,
'build.run_build': False,
}
@ -456,7 +457,7 @@ def _render_prole_cfg(inputs: dict, globals_to_save: dict, sections: dict, gener
sections_order = [
'Welcome', 'Dependencies', 'Network', 'System Environment',
'Monitoring', 'Kerberos Authentication', 'Optional Features', 'Database Creation',
'Docker Build', 'Initialize Cluster', 'Initialization Scripts',
'Initialize Cluster', 'Docker Build', 'Initialization Scripts',
'Prole DB Deploy', 'Install'
]
@ -732,8 +733,8 @@ class ProleInstaller:
("Network", "network_scan"),
("System Environment", "env_setup"),
("Database Creation", "init_password"),
("Docker Build", "init_db_build"),
("Start Cluster", "init_cluster"),
("Docker Build", "init_db_build"),
("Initialization Scripts", "init_scripts"),
("Kerberos Authentication", "kerberos_config"),
("Supabase", "supabase_config"),
@ -864,8 +865,8 @@ class ProleInstaller:
self._register_page('network_scan', None)
self._register_page('env_setup', None)
self._register_page('init_password', None)
self._register_page('init_db_build', None)
self._register_page('init_cluster', None)
self._register_page('init_db_build', None)
self._register_page('init_scripts', None)
self._register_page('kerberos_config', None)
self._register_page('supabase_config', None)
@ -1824,6 +1825,7 @@ class ProleInstaller:
self.safe_after(lambda: self._db_set_status("Updating OpenBao...", '#1d1d1f'))
env = self._script_env_for_namespace(ns)
env["AT_REST_ENCRYPTION_ENABLED"] = _bool_str(self.at_rest_encryption_enabled.get())
script_path = str(PROJECT_ROOT / "etc" / "init_openbao.sh")
rc2, out2 = self._run_cmd_capture(['bash', script_path, 'initialize'], env=env, stdin_text=f"{p1}\n")
self._db_log(out2)
@ -2134,6 +2136,7 @@ class ProleInstaller:
_set('kerberos_config.user', _get_var(self.kerberos_user, ''))
_set('kerberos_config.password', _get_var(self.kerberos_password, ''))
_set_bool('kerberos_config.test_connection', _action('kerberos_config.test_connection', DEFAULT_ACTION_FLAGS.get('kerberos_config.test_connection', False)))
_set_bool('kerberos_config.init_authority', _action('kerberos_config.init_authority', DEFAULT_ACTION_FLAGS.get('kerberos_config.init_authority', False)))
# Init scripts + deploy
_set_bool('init_scripts.run_scripts', _action('init_scripts.run_scripts', DEFAULT_ACTION_FLAGS.get('init_scripts.run_scripts', True)))
@ -2253,7 +2256,7 @@ class ProleInstaller:
sections = {k: self.prole_cfg_data.get(k, {}) for k in [
'Welcome', 'Dependencies', 'Network', 'System Environment',
'Kerberos Authentication', 'Optional Features', 'Database Creation',
'Docker Build', 'Initialize Cluster', 'Initialization Scripts',
'Initialize Cluster', 'Docker Build', 'Initialization Scripts',
'Prole DB Deploy', 'Install'
]}
@ -2647,6 +2650,15 @@ class ProleInstaller:
self._canvas_items.append(btn_window)
self._overlay_widgets.append(test_btn)
authority_btn = tk.Button(self.bg_canvas, text="Initialize Authority", command=self.run_kerberos_authority,
bg='#F5F5DC', fg='black', activebackground='#E5E5D5',
highlightbackground='#F5F5DC', highlightthickness=0,
relief='flat', font=('SF Pro Text', 11), padx=16, pady=8)
authority_window = self.bg_canvas.create_window(220, y, window=authority_btn, anchor='nw')
self._canvas_items.append(authority_window)
self._overlay_widgets.append(authority_btn)
self._kerberos_authority_button = authority_btn
y += 60
# Use tk.Text
self.kerberos_status_text = tk.Text(self.bg_canvas, width=100, height=12, font=('Menlo', 10),
@ -2657,7 +2669,7 @@ class ProleInstaller:
self._overlay_widgets.append(self.kerberos_status_text)
def _render_supabase_config_page(self):
# Letterhead at top right
# Letterhead at top right (matching welcome screen theme)
content_width = self.bg_canvas.winfo_width() or 975
right_margin = content_width - 48
@ -2665,55 +2677,89 @@ class ProleInstaller:
ui.canvas_text(self, right_margin, 85, "Infrastructure Automated.", fill='#6e6e73',
font=('SF Pro Text', 18), anchor='ne')
self._render_title('Supabase Deployment', y=150)
self._render_paragraph("Deploy Supabase to your cluster. This will run the Supabase initialization script to set up all required components.", y=200)
self._render_title('Prole::Supabase', y=150)
description = (
"Launches the full Supabase open-source stack locally, following the\n"
"README/DEVELOPERS guidance (Docker Compose). This is a multi-service deployment\n"
"(Postgres, Auth, Storage, Realtime, Kong, Studio, etc.)"
)
self._render_paragraph(description, y=210)
y = 280
self._supabase_status_label = ui.canvas_text(self, 48, y, "Status: Ready", fill='#6e6e73', font=('SF Pro Text', 12, 'bold'))
self._canvas_items.append(self._supabase_status_label)
self._supabase_status_var = tk.StringVar(value="Ready")
status_item = ui.canvas_text(self, 48, y, "Status: Ready", fill='black', font=('SF Pro Text', 12))
y += 40
def update_status_text(*args):
try:
self.bg_canvas.itemconfig(status_item, text=f"Status: {self._supabase_status_var.get()}")
except Exception:
pass
self._supabase_status_var.trace_add('write', update_status_text)
# Standardized Console Output
self._supabase_console = self._create_console_output(y=320, title="Deployment Output", width=880, height=380)
y = 740
# Deploy Supabase Button
self._supabase_deploy_button = tk.Button(self.bg_canvas, text="Deploy Supabase", command=self.run_supabase_deploy,
bg='#007aff', fg='white', activebackground='#0062cc',
highlightthickness=0, relief='flat', font=('SF Pro Text', 12, 'bold'),
padx=20, pady=10)
bg='#F5F5DC', fg='black', activebackground='#E5E5D5',
highlightbackground='#F5F5DC', highlightthickness=0,
relief='flat', font=('SF Pro Text', 11), padx=20, pady=10)
btn_window = self.bg_canvas.create_window(48, y, window=self._supabase_deploy_button, anchor='nw')
self._canvas_items.append(btn_window)
self._overlay_widgets.append(self._supabase_deploy_button)
y += 70
self._supabase_console = ui.scrolled_console(self.bg_canvas, width=800, height=400)
console_window = self.bg_canvas.create_window(48, y, window=self._supabase_console, anchor='nw')
self._canvas_items.append(console_window)
self._overlay_widgets.append(self._supabase_console)
self._canvas_items.append(btn_window)
def run_supabase_deploy(self):
if getattr(self, '_supabase_deploying', False):
return
self._supabase_deploying = True
self._supabase_deploy_button.configure(state='disabled')
self.bg_canvas.itemconfig(self._supabase_status_label, text="Status: Deploying...", fill='#007aff')
self._supabase_status_var.set("Deploying...")
self._supabase_console.clear()
def worker():
etc_dir = PROJECT_ROOT / "etc"
env = os.environ.copy()
env["PROLE_HOME"] = str(PROJECT_ROOT)
env["PROLE_SERVICE"] = str(PROJECT_ROOT)
env["NAMESPACE"] = (self.db_namespace.get() or '').strip()
self._supabase_console.write("Starting Supabase deployment...\n")
proc = self.controller.run_script('init_supabase.sh',
# Use supabase.sh as requested in the issue
# Mapping ~/dev/prole/supabase.sh to PROJECT_ROOT / "supabase.sh"
script_path = PROJECT_ROOT / "supabase.sh"
# If supabase.sh doesn't exist, we fallback to supabase/deploy.sh
# following the same pattern as controller.run_script fallback logic
if not script_path.exists():
deploy_fallback = PROJECT_ROOT / "supabase" / "deploy.sh"
if deploy_fallback.exists():
self._supabase_console.write(f"Warning: {script_path} not found. Falling back to {deploy_fallback}.\n")
script_to_run = "supabase/deploy.sh"
else:
self._supabase_console.write(f"Error: {script_path} not found.\n")
self.safe_after(lambda: self._supabase_status_var.set("Failed (Script not found)"))
self._supabase_deploying = False
self._supabase_deploy_button.configure(state='normal')
return
else:
script_to_run = "supabase.sh"
proc = self.controller.run_script(script_to_run,
on_line=lambda l: self._supabase_console.write(l + "\n"),
env=env)
if proc.returncode == 0:
self._supabase_success = True
self.bg_canvas.itemconfig(self._supabase_status_label, text="Status: Deployed Successfully", fill='#34c759')
self.safe_after(lambda: self._supabase_status_var.set("Deployed Successfully"))
self._supabase_console.write("\nSupabase deployment completed successfully.\n")
else:
self._supabase_success = False
self.bg_canvas.itemconfig(self._supabase_status_label, text=f"Status: Failed (Code {proc.returncode})", fill='#ff3b30')
self.safe_after(lambda: self._supabase_status_var.set(f"Failed (Code {proc.returncode})"))
self._supabase_console.write(f"\nSupabase deployment failed with exit code {proc.returncode}.\n")
self._supabase_deploying = False
self._supabase_deploy_button.configure(state='normal')
self.update_footer()
@ -2748,7 +2794,7 @@ class ProleInstaller:
pass
self.kerberos_status_text.delete('1.0', tk.END)
self.kerberos_status_text.insert(tk.END, f"Deploying OpenBao and testing {user}@{realm} inside the cluster...\n")
self.kerberos_status_text.insert(tk.END, f"Testing {user}@{realm} inside the cluster...\n")
def worker():
try:
@ -2775,19 +2821,6 @@ class ProleInstaller:
pass
self.safe_after(_do_write)
db_pass = self.db_password.get()
write_line("==> init_openbao.sh initialize\n")
rc1 = self.controller.run_script(
"init_openbao.sh",
args=["initialize"],
env=env,
stdin_text=f"{db_pass}\n" if db_pass else None,
on_line=write_line
)
if rc1 != 0:
write_line(f"\nOpenBao initialization failed with code {rc1}\n")
return
write_line("\n==> init_kerberos.sh test\n")
rc2 = self.controller.run_script(
"init_kerberos.sh",
@ -2811,6 +2844,88 @@ class ProleInstaller:
threading.Thread(target=worker, daemon=True).start()
def run_kerberos_authority(self):
self._action_flags['kerberos_config.init_authority'] = True
if not self.kerberos_enabled.get():
try:
messagebox.showerror("Kerberos", "Enable Kerberos authentication before initializing authority.")
except Exception:
pass
return
try:
self.prole_cfg_data['Kerberos Authentication']['ENABLED'] = str(self.kerberos_enabled.get())
self.prole_cfg_data['Kerberos Authentication']['REALM'] = self.kerberos_realm.get().strip()
self.prole_cfg_data['Kerberos Authentication']['KDC'] = self.kerberos_kdc.get().strip()
self.prole_cfg_data['Kerberos Authentication']['SERVER'] = self.kerberos_kdc.get().strip()
self.prole_cfg_data['Kerberos Authentication']['USER'] = self.kerberos_user.get().strip()
self.prole_cfg_data['Kerberos Authentication']['PASSWORD'] = self.kerberos_password.get().strip()
self._save_prole_cfg()
except Exception:
pass
# Guard: only run if saved config indicates Kerberos enabled
if not self.kerberos_enabled.get():
try:
messagebox.showerror("Kerberos", "Kerberos authentication must be enabled and saved in prole.cfg.")
except Exception:
pass
return
self.kerberos_status_text.delete('1.0', tk.END)
self.kerberos_status_text.insert(tk.END, "Initializing Kerberos authority...\n")
def worker():
env = os.environ.copy()
env["PROLE_HOME"] = str(PROJECT_ROOT)
env["PROLE_SERVICE"] = str(PROJECT_ROOT)
env["NAMESPACE"] = (self.db_namespace.get() or '').strip()
if self.kerberos_realm.get().strip():
env["KRB5_REALM"] = self.kerberos_realm.get().strip()
env["REALM"] = self.kerberos_realm.get().strip()
env["DOMAIN"] = self.kerberos_realm.get().strip().lower()
if self.kerberos_kdc.get().strip():
env["KRB5_KDC"] = self.kerberos_kdc.get().strip()
env["KRB5_ADMIN"] = self.kerberos_kdc.get().strip()
if self.kerberos_user.get().strip():
env["KRB5_USER"] = self.kerberos_user.get().strip()
if self.kerberos_password.get().strip():
env["KRB5_PASSWORD"] = self.kerberos_password.get().strip()
def write_line(line):
def _do_write(l=line):
try:
if self.kerberos_status_text.winfo_exists():
self.kerberos_status_text.insert(tk.END, l)
self.kerberos_status_text.see(tk.END)
except (tk.TclError, RuntimeError):
pass
self.safe_after(_do_write)
rc_bao = self.controller.run_script(
"init_openbao.sh",
args=["start"],
env=env,
on_line=write_line
)
if rc_bao != 0:
write_line(f"\nOpenBao start failed with code {rc_bao}\n")
return
write_line("\n==> init_authority.sh start\n")
rc = self.controller.run_script(
"init_authority.sh",
args=["start"],
env=env,
on_line=write_line
)
if rc == 0:
write_line("\nAuthority initialization completed successfully.\n")
else:
write_line(f"\nAuthority initialization failed with code {rc}\n")
threading.Thread(target=worker, daemon=True).start()
def _start_welcome_dependency_scan(self):
self.splash_scan_running = True
self.splash_scan_done_at = None
@ -3077,6 +3192,8 @@ class ProleInstaller:
def worker():
key_path = Path.home() / ".ssh" / "id_prole_ed25519"
key_path.parent.mkdir(parents=True, exist_ok=True)
password = self.db_password.get()
namespace = (self.db_namespace.get() or '').strip()
# Pre-pull dependent images into local registry before initialization
try:
@ -3093,8 +3210,8 @@ class ProleInstaller:
if key_path.exists():
console.write(f"Key already exists at {key_path}. Proceeding...\n")
self.safe_after(lambda: self.bg_canvas.itemconfig(status_label, text="Environment ready. Proceeding...", fill='#34c759') if self.bg_canvas.winfo_exists() else None)
time.sleep(1.5)
self.safe_after(lambda: self.bg_canvas.itemconfig(status_label, text="Keys present. Proceeding...", fill='#34c759') if self.bg_canvas.winfo_exists() else None)
time.sleep(0.5)
else:
self.safe_after(lambda: self.bg_canvas.itemconfig(status_label, text="Preparing keys...", fill='blue') if self.bg_canvas.winfo_exists() else None)
cmd = ["ssh-keygen", "-t", "ed25519", "-N", "", "-f", str(key_path), "-C", self.db_username.get()]
@ -3128,9 +3245,33 @@ class ProleInstaller:
self.safe_after(lambda: self.bg_canvas.itemconfig(status_label, text=f"Error during preparation (code {proc.returncode})", fill='#ff3b30') if self.bg_canvas.winfo_exists() else None)
# In case of error, we might want to let the user see it before continuing or stopping
time.sleep(2)
self.root.after(500, lambda: self.show_page('init_password'))
return
# Initialize OpenBao and store keys/passwords
self.safe_after(lambda: self.bg_canvas.itemconfig(status_label, text="Initializing OpenBao...", fill='blue') if self.bg_canvas.winfo_exists() else None)
env = self._script_env_for_namespace(namespace)
env["PROLE_DB_USER"] = self.db_username.get().strip()
env["DB_PASSWORD"] = password
env["AT_REST_ENCRYPTION_ENABLED"] = _bool_str(self.at_rest_encryption_enabled.get())
if self.at_rest_encryption_enabled.get():
console.write("At-rest encryption enabled: generating/storing TDE keys in OpenBao...\n")
rc = self.controller.run_script(
"init_openbao.sh",
args=["initialize"],
env=env,
stdin_text=f"{password}\n",
on_line=lambda l: console.write(l)
)
if rc != 0:
console.write(f"\nOpenBao initialization failed (code {rc}).\n")
self.safe_after(lambda: self.bg_canvas.itemconfig(status_label, text=f"OpenBao init failed (code {rc})", fill='#ff3b30') if self.bg_canvas.winfo_exists() else None)
self.root.after(500, lambda: self.show_page('init_password'))
return
# Move to next page
self.root.after(1000, lambda: self.show_page('init_db_build'))
self.safe_after(lambda: self.bg_canvas.itemconfig(status_label, text="OpenBao ready. Proceeding...", fill='#34c759') if self.bg_canvas.winfo_exists() else None)
self.root.after(1000, lambda: self.show_page('init_cluster'))
threading.Thread(target=worker, daemon=True).start()
@ -3144,7 +3285,7 @@ class ProleInstaller:
font=('SF Pro Text', 18), anchor='ne')
self._render_title('Start Cluster', y=150)
self._render_paragraph('Select a cluster environment and ensure Docker and K3D are running.', y=200)
self._render_paragraph('Select a cluster environment and ensure Docker, K3D, OpenBao, and the local registry are running.', y=200)
# Cluster Selection (Radio Buttons)
x_label = 48
@ -3177,6 +3318,14 @@ class ProleInstaller:
self.k3d_status_label = ui.canvas_text(self, x_label, y, 'K3D Cluster: Checking...', fill='#6e6e73', font=('SF Pro Text', 12))
self._canvas_items.append(self.k3d_status_label)
y += 30
self.registry_status_label = ui.canvas_text(self, x_label, y, 'Local Registry: Checking...', fill='#6e6e73', font=('SF Pro Text', 12))
self._canvas_items.append(self.registry_status_label)
y += 30
self.openbao_status_label = ui.canvas_text(self, x_label, y, 'OpenBao: Checking...', fill='#6e6e73', font=('SF Pro Text', 12))
self._canvas_items.append(self.openbao_status_label)
y += 50
# Use tk.Button on canvas
btn = tk.Button(self.bg_canvas, text='Start Cluster', command=self.ensure_cluster_ready,
@ -3203,11 +3352,39 @@ class ProleInstaller:
k3d_msg = f"K3D Cluster ({cluster_name}): Running" if k3d_ok else f"K3D Cluster ({cluster_name}): Not found/stopped"
k3d_fill = '#34c759' if k3d_ok else '#ff9f0a'
registry_ok = False
openbao_ok = False
openbao_name = os.environ.get("OPENBAO_NAME", "openbao")
if docker_ok:
try:
ps = subprocess.run(['docker', 'ps', '--format', '{{.Names}} {{.Image}}'], capture_output=True, text=True)
for line in (ps.stdout or '').splitlines():
parts = line.split()
if not parts:
continue
name = parts[0]
image = parts[1] if len(parts) > 1 else ''
if name == 'k3d-prole-registry' or image.startswith('registry:2'):
registry_ok = True
if name == openbao_name:
openbao_ok = True
except Exception:
pass
registry_msg = 'Local Registry: Running' if registry_ok else 'Local Registry: Not running'
registry_fill = '#34c759' if registry_ok else '#ff9f0a'
openbao_msg = 'OpenBao: Running' if openbao_ok else 'OpenBao: Not running'
openbao_fill = '#34c759' if openbao_ok else '#ff9f0a'
def update_ui():
if hasattr(self, 'docker_status_label'):
self.bg_canvas.itemconfig(self.docker_status_label, text=docker_msg, fill=docker_fill)
if hasattr(self, 'k3d_status_label'):
self.bg_canvas.itemconfig(self.k3d_status_label, text=k3d_msg, fill=k3d_fill)
if hasattr(self, 'registry_status_label'):
self.bg_canvas.itemconfig(self.registry_status_label, text=registry_msg, fill=registry_fill)
if hasattr(self, 'openbao_status_label'):
self.bg_canvas.itemconfig(self.openbao_status_label, text=openbao_msg, fill=openbao_fill)
if hasattr(self, '_init_cluster_button'):
label = 'Verify Cluster' if k3d_ok else 'Start Cluster'
try:
@ -3237,7 +3414,26 @@ class ProleInstaller:
self.root.after(0, lambda: messagebox.showerror('Docker', 'Could not start Docker. Please start it manually.'))
return
# 2. Manage K3D Cluster
# 2. Ensure OpenBao container is running (local-only)
env = self._script_env_for_namespace((self.db_namespace.get() or '').strip())
env["PROLE_DB_USER"] = self.db_username.get().strip()
env["AT_REST_ENCRYPTION_ENABLED"] = _bool_str(self.at_rest_encryption_enabled.get())
rc_openbao = self.controller.run_script("init_openbao.sh", args=["start"], env=env)
if rc_openbao != 0:
self.root.after(0, lambda: messagebox.showerror('OpenBao', f'Failed to start OpenBao (code {rc_openbao}).'))
return
# 3. Ensure local registry is available before cluster creation (dev only)
if self.cluster_env.get() == 'dev':
try:
reg_info = self.ensure_local_registry_available()
except Exception:
reg_info = None
if not reg_info:
self.root.after(0, lambda: messagebox.showerror('Registry', 'Local registry is not running. Please start the registry and try again.'))
return
# 4. Manage K3D Cluster
cluster_name = f"prole-{self.cluster_env.get()}-cluster"
res = subprocess.run(['k3d', 'cluster', 'list', '--no-headers'], capture_output=True, text=True)
if cluster_name not in res.stdout:
@ -3741,13 +3937,11 @@ class ProleInstaller:
def _init_scripts_list(self):
scripts = [
('Kubectl Status', self.KUBECTL_STATUS_TAB),
('OpenBao', 'init_openbao.sh'),
('Garage Store', 'init_garage_store.sh'),
('Garage Log', 'garage_log'),
('CloudNative-PG', 'init_cloudnative_pg.sh'),
]
if self.kerberos_enabled.get():
scripts.append(('Authority', 'init_authority.sh'))
scripts.append(('Kerberos Realm', 'init_kerberos.sh'))
scripts.extend([
('Prole DB', 'init_prole-db.sh'),
@ -3767,7 +3961,7 @@ class ProleInstaller:
font=('SF Pro Text', 18), anchor='ne')
self._render_title('Initialization Scripts', y=150)
self._render_paragraph('Running initialization scripts to set up OpenBao, Garage, CloudNative-PG, and backups.', y=200)
self._render_paragraph('Running initialization scripts to set up Garage, CloudNative-PG, and backups.', y=200)
# Tabs for output - using standardized appearance
ui.canvas_text(self, 48, 260, "Execution Output", fill='#1d1d1f', font=('SF Pro Text', 12, 'bold'))
@ -3979,42 +4173,18 @@ class ProleInstaller:
return logs_dir / 'init_garage_store.log'
return logs_dir / f'{base}.log'
# 1. init_openbao.sh initialize
script = "init_openbao.sh"
_select_tab(script)
self.script_consoles[script].clear()
self.script_consoles[script].write(f"Running {script} initialize...\n")
log_path = _log_path_for(script)
self._record_install_log(log_path)
try:
log_fp = log_path.open('a', encoding='utf-8')
except Exception:
log_fp = None
def _openbao_line(line):
self.script_consoles[script].write(line)
if log_fp:
try:
log_fp.write(line)
log_fp.flush()
except Exception:
pass
rc1 = self.controller.run_script(
script,
args=['initialize'],
env=env,
stdin_text=f"{password}\n",
on_line=_openbao_line
# 0. Pre-emptive stop for port-forwards to avoid conflicts on re-install
script_pf = "init_port_forwards.sh"
if script_pf in self.script_consoles:
self.script_consoles[script_pf].write("Stopping existing port-forwards to avoid conflicts...\n")
self.controller.run_script(
script_pf,
args=['stop'],
env=env
)
if log_fp:
try:
log_fp.close()
except Exception:
pass
# 2. init_garage_store.sh start
overall_success = (rc1 == 0)
# 1. init_garage_store.sh start
overall_success = True
if overall_success:
script = "init_garage_store.sh"
_select_tab(script)
@ -4061,9 +4231,9 @@ class ProleInstaller:
else:
self.script_consoles[script].write(f"\n{script} completed successfully.\n")
else:
self.script_consoles["init_garage_store.sh"].write("Skipping Garage initialization because OpenBao initialization failed.\n")
self.script_consoles["init_garage_store.sh"].write("Skipping Garage initialization because previous steps failed.\n")
# 3. init_cloudnative_pg.sh initialize
# 2. init_cloudnative_pg.sh initialize
if overall_success:
script = "init_cloudnative_pg.sh"
_select_tab(script)
@ -4108,49 +4278,7 @@ class ProleInstaller:
if "init_cloudnative_pg.sh" in self.script_consoles:
self.script_consoles["init_cloudnative_pg.sh"].write("Skipping CloudNative-PG initialization because previous steps failed.\n")
# 4. init_authority.sh start (optional)
if overall_success and self.kerberos_enabled.get():
script = "init_authority.sh"
if script in self.script_consoles:
_select_tab(script)
self.script_consoles[script].clear()
self.script_consoles[script].write(f"Running {script} start...\n")
log_path = _log_path_for(script)
self._record_install_log(log_path)
try:
log_fp = log_path.open('a', encoding='utf-8')
except Exception:
log_fp = None
def _auth_line(line):
self.script_consoles[script].write(line)
if log_fp:
try:
log_fp.write(line)
log_fp.flush()
except Exception:
pass
rc_auth = self.controller.run_script(
script,
args=['start'],
env=env,
on_line=_auth_line
)
if log_fp:
try:
log_fp.close()
except Exception:
pass
if rc_auth != 0:
self.script_consoles[script].write(f"\nERROR: {script} start failed with code {rc_auth}\n")
overall_success = False
else:
self.script_consoles[script].write(f"\n{script} completed successfully.\n")
# 5. init_kerberos.sh initialize (optional)
# 3. init_kerberos.sh initialize (optional)
if overall_success and self.kerberos_enabled.get():
script = "init_kerberos.sh"
if script in self.script_consoles:
@ -4722,14 +4850,14 @@ class ProleInstaller:
if current_id == 'init_password':
self.show_page('env_setup')
return
if current_id == 'init_db_build':
if current_id == 'init_cluster':
self.show_page('init_password')
return
if current_id == 'init_cluster':
self.show_page('init_db_build')
if current_id == 'init_db_build':
self.show_page('init_cluster')
return
if current_id == 'init_scripts':
self.show_page('init_cluster')
self.show_page('init_db_build')
return
if current_id == 'kerberos_config':
self.show_page('init_scripts')
@ -4847,6 +4975,24 @@ class ProleInstaller:
self.show_page('init_password')
return
if current_id == 'init_password':
p1 = self.db_password.get()
p2 = self.db_password_confirm.get()
if not p1:
try:
messagebox.showerror('Password', 'Password cannot be empty.')
except Exception:
pass
return
if p1 != p2:
try:
messagebox.showerror('Password', 'Passwords do not match.')
except Exception:
pass
return
self._run_preparation_overlay()
return
if current_id == 'init_cluster':
# Capture cluster info
self.prole_cfg_data['Initialize Cluster']['ENVIRONMENT'] = self.cluster_env.get()
@ -4861,7 +5007,7 @@ class ProleInstaller:
except Exception:
pass
return
self.show_page('init_scripts')
self.show_page('init_db_build')
return
if current_id == 'init_scripts':
@ -7764,6 +7910,7 @@ class ProleSilentInstaller:
inputs['kerberos_config.user'] = ''
inputs['kerberos_config.password'] = ''
inputs['kerberos_config.test_connection'] = _bool_str(DEFAULT_ACTION_FLAGS.get('kerberos_config.test_connection', False))
inputs['kerberos_config.init_authority'] = _bool_str(DEFAULT_ACTION_FLAGS.get('kerberos_config.init_authority', False))
# Init scripts + deploy
inputs['init_scripts.run_scripts'] = _bool_str(DEFAULT_ACTION_FLAGS.get('init_scripts.run_scripts', True))
@ -8226,7 +8373,7 @@ class ProleSilentInstaller:
sections = {k: self.prole_cfg_data.get(k, {}) for k in [
'Welcome', 'Dependencies', 'Network', 'System Environment',
'Kerberos Authentication', 'Optional Features', 'Database Creation',
'Docker Build', 'Initialize Cluster', 'Initialization Scripts',
'Initialize Cluster', 'Docker Build', 'Initialization Scripts',
'Prole DB Deploy', 'Install'
]}
cfg_text = _render_prole_cfg(self.inputs, globals_to_save, sections)
@ -8402,6 +8549,45 @@ class ProleSilentInstaller:
if self._get_input_bool('init_password.generate_ssh_key', DEFAULT_ACTION_FLAGS.get('init_password.generate_ssh_key', True)):
self._generate_ssh_key(user)
# Initialize OpenBao and store keys/passwords for this namespace
if not self.controller.check_docker_running():
if platform.system() == 'Darwin':
self.log("Starting Docker...")
self._run_cmd(['open', '-a', 'Docker'])
for _ in range(30):
time.sleep(2)
if self.controller.check_docker_running():
break
if not self.controller.check_docker_running():
raise Exception("Docker is not running; OpenBao requires Docker.")
env = os.environ.copy()
env["PROLE_HOME"] = str(self.project_root)
env["PROLE_SERVICE"] = str(self.project_root)
env["PROLE_DB_USER"] = user
env["DB_PASSWORD"] = p1
env["NAMESPACE"] = ns
env["AT_REST_ENCRYPTION_ENABLED"] = _bool_str(self._get_input_bool('init_cluster.at_rest_encryption_enabled', False))
realm = self._get_input('kerberos_config.realm', '').strip()
kdc = self._get_input('kerberos_config.kdc', '').strip()
krb_user = self._get_input('kerberos_config.user', '').strip()
krb_pw = self._get_input('kerberos_config.password', '').strip()
if realm:
env["KRB5_REALM"] = realm
env["REALM"] = realm
env["DOMAIN"] = realm.lower()
if kdc:
env["KRB5_KDC"] = kdc
env["KRB5_ADMIN"] = kdc
if krb_user:
env["KRB5_USER"] = krb_user
if krb_pw:
env["KRB5_PASSWORD"] = krb_pw
self.log("==> OpenBao initialize")
rc_bao = self._run_script("init_openbao.sh", args=["initialize"], env=env, stdin_text=f"{p1}\n")
if rc_bao != 0:
raise Exception(f"OpenBao initialization failed (code {rc_bao})")
def _generate_ssh_key(self, user: str):
self.log("==> Environment Preparation (Secure Access)")
key_path = Path.home() / ".ssh" / "id_prole_ed25519"
@ -8482,6 +8668,21 @@ class ProleSilentInstaller:
if not self.controller.check_docker_running():
raise Exception("Docker is not running.")
# Ensure OpenBao container is running (local-only)
env = os.environ.copy()
env["PROLE_HOME"] = str(self.project_root)
env["PROLE_SERVICE"] = str(self.project_root)
env["NAMESPACE"] = (self._get_input('init_password.db_namespace', '') or '').strip()
env["AT_REST_ENCRYPTION_ENABLED"] = _bool_str(self._get_input_bool('init_cluster.at_rest_encryption_enabled', False))
rc_bao = self._run_script("init_openbao.sh", args=["start"], env=env)
if rc_bao != 0:
raise Exception(f"OpenBao start failed (code {rc_bao})")
# Ensure local registry is available before cluster creation (dev only)
if cluster_env == 'dev':
if not self._ensure_local_registry_available():
raise Exception("Local registry is not running.")
cluster_name = f"prole-{cluster_env}-cluster"
res = subprocess.run(['k3d', 'cluster', 'list', '--no-headers'], capture_output=True, text=True)
if cluster_name not in res.stdout:
@ -8523,13 +8724,6 @@ class ProleSilentInstaller:
self.prole_cfg_data['Kerberos Authentication']['AD_PROXY_IMAGE'] = os.environ.get('KRB5_AD_PROXY_IMAGE', 'alpine/socat')
self.prole_cfg_data['Kerberos Authentication']['AD_PROXY_SERVICE'] = os.environ.get('KRB5_AD_SERVICE_NAME', 'prole-kerberos-ad-dc')
if not self._get_input_bool('kerberos_config.test_connection', DEFAULT_ACTION_FLAGS.get('kerberos_config.test_connection', False)):
self.log("[SKIP] Kerberos test disabled.")
return
if not (realm and user and password and kdc):
self.err("[WARN] Kerberos test skipped: missing realm/user/password/kdc.")
return
env = os.environ.copy()
env["PROLE_HOME"] = str(self.project_root)
env["PROLE_SERVICE"] = str(self.project_root)
@ -8543,11 +8737,25 @@ class ProleSilentInstaller:
env["KRB5_PASSWORD"] = password
env.setdefault("KRB5_AD_PORT_FORWARD", "1")
self.log("==> Kerberos test: init_openbao.sh initialize")
rc1 = self._run_script("init_openbao.sh", args=["initialize"], env=env, stdin_text=f"{self._get_input('init_password.db_password', '')}\n")
if rc1 != 0:
self.err(f"[ERROR] OpenBao initialization failed (code {rc1})")
if self._get_input_bool('kerberos_config.init_authority', False):
self.log("==> Kerberos authority: init_authority.sh start")
rc_auth_bao = self._run_script("init_openbao.sh", args=["start"], env=env)
if rc_auth_bao != 0:
self.err(f"[ERROR] OpenBao start failed (code {rc_auth_bao})")
else:
rc_auth = self._run_script("init_authority.sh", args=["start"], env=env)
if rc_auth != 0:
self.err(f"[ERROR] Authority initialization failed (code {rc_auth})")
else:
self.log("[OK] Authority initialization completed successfully.")
if not self._get_input_bool('kerberos_config.test_connection', DEFAULT_ACTION_FLAGS.get('kerberos_config.test_connection', False)):
self.log("[SKIP] Kerberos test disabled.")
return
if not (realm and user and password and kdc):
self.err("[WARN] Kerberos test skipped: missing realm/user/password/kdc.")
return
self.log("==> Kerberos test: init_kerberos.sh test")
rc2 = self._run_script("init_kerberos.sh", args=["test"], env=env)
if rc2 != 0:
@ -8586,8 +8794,10 @@ class ProleSilentInstaller:
env["KRB5_PASSWORD"] = krb_pw
env.setdefault("KRB5_AD_PORT_FORWARD", "1")
self.log("Stopping existing port-forwards to avoid conflicts...")
self._run_script("init_port_forwards.sh", args=["stop"], env=env)
steps = [
("init_openbao.sh", ["initialize"], True),
("init_garage_store.sh", ["start"], False),
("init_cloudnative_pg.sh", ["initialize"], False),
]
@ -8659,7 +8869,7 @@ class ProleSilentInstaller:
env["PROLE_HOME"] = str(self.project_root)
env["PROLE_SERVICE"] = str(self.project_root)
env["NAMESPACE"] = (self._get_input('init_password.db_namespace', '') or '').strip()
rc = self._run_script('init_supabase.sh', env=env)
rc = self._run_script('supabase.sh', env=env)
if rc == 0:
self._supabase_success = True
self.prole_cfg_data['Supabase'] = {'STATUS': 'Deployed'}
@ -8694,10 +8904,10 @@ class ProleSilentInstaller:
self._step_init_password()
self._write_cfg()
self._step_db_build()
self._step_cluster()
self._write_cfg()
self._step_cluster()
self._step_db_build()
self._write_cfg()
self._step_init_scripts()

View File

@ -116,10 +116,10 @@ class ProleNcursesInstaller:
("network_scan", self._render_network_scan),
("env_setup", self._render_env_setup),
("init_password", self._render_init_password),
("init_db_build", self._render_init_db_build),
("init_cluster", self._render_init_cluster),
("kerberos_config", self._render_kerberos_config),
("init_db_build", self._render_init_db_build),
("init_scripts", self._render_init_scripts),
("kerberos_config", self._render_kerberos_config),
("init_cnpg_deploy", self._render_init_cnpg_deploy),
("create_installer", self._render_create_installer),
]
@ -189,11 +189,11 @@ class ProleNcursesInstaller:
("Dependencies", "deps_summary"),
("Network Scan", "network_scan"),
("Environment", "env_setup"),
("Kerberos", "kerberos_config"),
("Password", "init_password"),
("Build", "init_db_build"),
("Cluster", "init_cluster"),
("Build", "init_db_build"),
("Scripts", "init_scripts"),
("Kerberos", "kerberos_config"),
("Deploy", "init_cnpg_deploy"),
("Installer", "create_installer"),
]

1
supabase.sh Symbolic link
View File

@ -0,0 +1 @@
supabase/deploy.sh

246
tests/silent_install_test.sh Executable file
View File

@ -0,0 +1,246 @@
#!/bin/bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# Logging setup
LOG_DIR="${ROOT_DIR}/logs"
mkdir -p "$LOG_DIR"
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
LOG_FILE="${LOG_DIR}/silent_install_${TIMESTAMP}.log"
# Function to handle output redirection
if [[ "${SILENT_INSTALL_LOG:-}" == "true" ]]; then
echo "Logging output to: $LOG_FILE"
# Redirect stdout and stderr to both terminal and log file
exec > >(tee -a "$LOG_FILE") 2>&1
fi
CFG_PATH="${1:-}"
if [[ -z "$CFG_PATH" ]]; then
if [[ -n "${PROLE_CONF:-}" && -f "$PROLE_CONF/prole.cfg" ]]; then
CFG_PATH="$PROLE_CONF/prole.cfg"
elif [[ -f "$ROOT_DIR/conf/prole.cfg" ]]; then
CFG_PATH="$ROOT_DIR/conf/prole.cfg"
elif [[ -f "$ROOT_DIR/prole/conf/prole.cfg" ]]; then
CFG_PATH="$ROOT_DIR/prole/conf/prole.cfg"
else
echo "ERROR: prole.cfg not found. Pass a path or set PROLE_CONF." >&2
exit 1
fi
fi
echo "=============================================="
echo " SILENT INSTALL TEST"
echo " prole.cfg: $CFG_PATH"
echo "=============================================="
echo ""
echo "1. Validating prole.cfg..."
python3 - "$CFG_PATH" <<'PY'
import configparser
import os
import sys
from pathlib import Path
cfg_path = Path(sys.argv[1]).expanduser()
if not cfg_path.exists():
print(f"ERROR: prole.cfg not found at {cfg_path}", file=sys.stderr)
sys.exit(1)
cfg = configparser.ConfigParser(interpolation=None)
cfg.optionxform = str
cfg.read(cfg_path)
required_sections = ["Inputs", "Global"]
missing_sections = [s for s in required_sections if not cfg.has_section(s)]
if missing_sections:
print(f"ERROR: Missing sections: {', '.join(missing_sections)}", file=sys.stderr)
sys.exit(1)
required_inputs = [
"build.deploy_env",
"build.run_build",
"dependencies.auto_install_missing",
"dependencies.brew.install",
"dependencies.docker.install",
"dependencies.k3d.install",
"dependencies.verify_all",
"disk_selection.disk_type",
"disk_selection.removable_mount",
"disk_selection.local_path",
"env_setup.NAMESPACE",
"env_setup.PROLE_CONF",
"env_setup.PROLE_DATA",
"env_setup.PROLE_HOME",
"env_setup.PROLE_LOGS",
"env_setup.PROLE_SERVICE",
"init_cluster.at_rest_encryption_enabled",
"init_cluster.cluster_env",
"init_cluster.kerberos_enabled",
"init_cluster.start_cluster",
"init_cluster.supabase_enabled",
"init_cnpg_deploy.force_rollout",
"init_cnpg_deploy.run_deploy",
"init_db_build.run_build",
"init_password.db_host_port",
"init_password.db_namespace",
"init_password.db_password",
"init_password.db_password_confirm",
"init_password.db_username",
"init_password.generate_ssh_key",
"init_scripts.run_scripts",
"kerberos_config.enabled",
"kerberos_config.init_authority",
"kerberos_config.kdc",
"kerberos_config.password",
"kerberos_config.realm",
"kerberos_config.test_connection",
"kerberos_config.user",
"network_scan.run",
]
missing_inputs = [k for k in required_inputs if not cfg.has_option("Inputs", k)]
if missing_inputs:
print("ERROR: Missing Inputs keys:", file=sys.stderr)
for key in missing_inputs:
print(f" - {key}", file=sys.stderr)
sys.exit(1)
required_non_empty = {
"Inputs": [
"env_setup.PROLE_HOME",
"env_setup.PROLE_CONF",
"env_setup.PROLE_DATA",
"env_setup.PROLE_LOGS",
"env_setup.PROLE_SERVICE",
"env_setup.NAMESPACE",
"init_password.db_namespace",
"init_password.db_username",
"init_password.db_host_port",
],
"Global": [
"PROLE_HOME",
"PROLE_DB_USER",
"CLUSTER_ENV",
"NAMESPACE",
],
}
def is_anchor(val: str) -> bool:
val = val.strip()
return val.startswith("${OPENBAO:") or val.startswith("${PROLE_SECRET:")
errors = []
warnings = []
for section, keys in required_non_empty.items():
for key in keys:
if not cfg.has_option(section, key):
errors.append(f"{section}.{key} missing")
continue
value = cfg.get(section, key, fallback="").strip()
if not value:
errors.append(f"{section}.{key} is empty")
secret_keys = {
("Inputs", "init_password.db_password"),
("Inputs", "init_password.db_password_confirm"),
("Global", "DB_PASSWORD"),
}
for section, key in secret_keys:
if not cfg.has_option(section, key):
errors.append(f"{section}.{key} missing")
continue
value = cfg.get(section, key, fallback="").strip()
if not value:
errors.append(f"{section}.{key} is empty")
elif is_anchor(value):
warnings.append(f"{section}.{key} is anchored ({value})")
pw = cfg.get("Inputs", "init_password.db_password", fallback="")
pw_confirm = cfg.get("Inputs", "init_password.db_password_confirm", fallback="")
if pw and pw_confirm and pw != pw_confirm:
errors.append("Inputs.init_password.db_password does not match init_password.db_password_confirm")
if errors:
print("ERROR: prole.cfg validation failed:", file=sys.stderr)
for err in errors:
print(f" - {err}", file=sys.stderr)
sys.exit(1)
if warnings:
print("WARN: prole.cfg contains anchored secrets:")
for warn in warnings:
print(f" - {warn}")
print("OK: prole.cfg validation")
PY
echo ""
echo "2. Running silent install..."
if python3 "$ROOT_DIR/install.py" -S -c "$CFG_PATH"; then
echo "OK: Silent install completed"
else
echo "ERROR: Silent install failed" >&2
exit 1
fi
echo ""
echo "3. Blessing sanitized gold config..."
python3 - "$CFG_PATH" "$ROOT_DIR/prole/conf/prole-db/prole.cfg" <<'PY'
import re
import sys
from pathlib import Path
source = Path(sys.argv[1]).expanduser()
target = Path(sys.argv[2]).expanduser()
target.parent.mkdir(parents=True, exist_ok=True)
text = source.read_text()
secret_map = {
"init_password.db_password": "${OPENBAO:prole/db_password}",
"init_password.db_password_confirm": "${OPENBAO:prole/db_password}",
"kerberos_config.password": "${OPENBAO:kerberos/password}",
"DB_PASSWORD": "${OPENBAO:prole/db_password}",
"PASSWORD": "${OPENBAO:kerberos/password}",
"GRAFANA_ADMIN_PASSWORD": "${OPENBAO:monitoring/grafana_admin_password}",
}
pattern = re.compile(r"^(?P<lead>\s*)(?P<key>[^=]+?)(?P<pre>\s*)=(?P<post>\s*).*$")
out_lines = []
for line in text.splitlines():
stripped = line.strip()
if not stripped or stripped.startswith((';', '#')) or '=' not in line:
out_lines.append(line)
continue
m = pattern.match(line)
if not m:
out_lines.append(line)
continue
key = m.group("key").strip()
lead = m.group("lead")
pre = m.group("pre")
post = m.group("post")
if key in secret_map:
value = secret_map[key]
out_lines.append(f"{lead}{key}{pre}={post}{value}")
continue
if re.search(r"(?i)(password|token|secret)", key):
out_lines.append(f"{lead}{key}{pre}={post}${{OPENBAO:REDACTED}}")
continue
out_lines.append(line)
target.write_text("\n".join(out_lines) + "\n")
print(f"OK: Wrote sanitized gold config: {target}")
PY
echo ""
echo "=============================================="
echo " OK: Silent Install Test Completed"
echo "=============================================="
echo ""