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> <portMappings>
<mapping id="dashboard" namespace="kubernetes-dashboard" target="svc/kubernetes-dashboard-kong-proxy" address="127.0.0.1" hostPort="8443" servicePort="443" protocol="TCP" description="Kubernetes Dashboard (https://127.0.0.1:8443)"/> <mapping id="dashboard" namespace="kubernetes-dashboard" target="svc/kubernetes-dashboard-kong-proxy" address="127.0.0.1" hostPort="8443" servicePort="443" protocol="TCP" description="Kubernetes Dashboard (https://127.0.0.1:8443)"/>
<mapping id="prometheus" namespace="${NAMESPACE}" target="svc/prometheus-community-kube-prometheus" address="127.0.0.1" hostPort="9090" servicePort="9090" protocol="TCP" description="Prometheus UI (http://127.0.0.1:9090)"/> <mapping id="prometheus" namespace="${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="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="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="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> </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_DATA="/Users/chrisfu/dev/prole/data"
export PROLE_LOGS="/Users/chrisfu/dev/prole/logs" export PROLE_LOGS="/Users/chrisfu/dev/prole/logs"
export PROLE_SERVICE="/Users/chrisfu/dev/prole/etc" 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.) # Ensure PATH works for GUI-launched shells (Docker, etc.)
_prole_add_path() { case ":${PATH}:" in *":$1:"*) ;; *) PATH="$1:${PATH:-}" ;; esac; } _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 # ./init_cloudnative_pg.sh update|reload # re-apply/patch
# #
# Requirements: # 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 # - $PROLE_HOME/env.sh or $HOME/.prole/env.sh defining PROLE_SERVICE
# - Optional: CNPG_MANIFEST_OVERRIDE to apply a recovery manifest instead of kustomize # - Optional: CNPG_MANIFEST_OVERRIDE to apply a recovery manifest instead of kustomize
@ -310,11 +310,6 @@ initialize() {
ensure_cnpg_operator ensure_cnpg_operator
echo "Using namespace: $NAMESPACE" 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 fetch_admin_keys_and_db_pass_from_bao_or_local
apply_cnpg_admin_secret apply_cnpg_admin_secret
@ -341,7 +336,8 @@ initialize() {
# Ensure port-forward is running for Postgres (local access) # Ensure port-forward is running for Postgres (local access)
echo "Ensuring port-forward for Postgres is active ..." 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 sleep 2
echo "Initialization complete for CNPG + Kerberos + cert artifacts." echo "Initialization complete for CNPG + Kerberos + cert artifacts."

View File

@ -215,14 +215,17 @@ init_layout() {
assign_count=$((assign_count + 1)) assign_count=$((assign_count + 1))
done done
# Re-fetch layout and apply staged changes using the current layout version # Re-fetch layout and apply staged changes using the NEXT layout version
local layout_after version apply_ok local layout_after version next_version apply_ok
layout_after=$(kubectl exec -n "$NAMESPACE" "$pod" -- /garage layout show 2>/dev/null || true) layout_after=$(kubectl exec -n "$NAMESPACE" "$pod" -- /garage layout show 2>/dev/null | grep -v "INFO" || true)
version=$(echo "$layout_after" | awk -F: '/[Vv]ersion/ {gsub(/ /,"",$2); print $2; exit}' || 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 echo "$layout_after" | grep -qi "staged"; then
if [[ -n "$version" ]]; then if [[ -n "$version" ]]; then
echo "Applying Garage layout (version $version) ..." next_version=$((version + 1))
if kubectl exec -n "$NAMESPACE" "$pod" -- /garage layout apply --version "$version"; then 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 apply_ok=1
else else
apply_ok=0 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 if kubectl -n "$NAMESPACE" get configmap prole-krb5-conf >/dev/null 2>&1; then
return return
fi fi
echo "ConfigMap prole-krb5-conf not found. Running init_openbao.sh update..." echo "ConfigMap prole-krb5-conf not found. Running init_kerberos.sh update..."
if [[ -x "$SCRIPT_DIR/init_openbao.sh" ]]; then if [[ -x "$SCRIPT_DIR/init_kerberos.sh" ]]; then
KRB5_REALM="$KRB5_REALM" KRB5_KDC="$KRB5_KDC" "$SCRIPT_DIR/init_openbao.sh" update || true KRB5_REALM="$KRB5_REALM" KRB5_KDC="$KRB5_KDC" "$SCRIPT_DIR/init_kerberos.sh" update || true
fi 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 # init_openbao.sh
# Purpose: # Purpose:
# - Deploy OpenBao to Kubernetes (dev mode) and store admin key pair # - Run OpenBao as a local Docker container (dev mode) and store admin key pair
# Generate and apply a Kerberos krb5.conf ConfigMap for an external realm # - Optionally generate Kerberos krb5.conf ConfigMap for external realm (legacy k8s flow)
# - Local Docker helpers for OpenBao (optional) # - Store secrets in OpenBao kv (admin keys, user keys, db password, optional TDE keys)
# Initialize SCRIPT_DIR # Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 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" root_token_file="$SECRETS_DIR/openbao-root-token"
ensure_tools() { 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; } command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
done done
} }
ensure_k8s_tools() {
command -v kubectl >/dev/null || { echo "Missing required tool: kubectl" >&2; exit 1; }
}
ensure_namespace() { ensure_namespace() {
if ! kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then if ! kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then
echo "Creating namespace '$NAMESPACE' ..." echo "Creating namespace '$NAMESPACE' ..."
@ -137,6 +141,31 @@ EOF
fi 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() { detect_openbao_root_token() {
local token="" local token=""
local file local file
@ -241,7 +270,8 @@ init_openbao_kv_and_store_admin_key() {
local token local token
token=$(cat "$root_token_file") token=$(cat "$root_token_file")
# Enable kv (if not already) and write keys # 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 ..." echo "Configuring KV at $svc ..."
# Check if kv/ is already mounted # 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." echo "Stored database password in OpenBao kv/prole/db."
fi 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 local krb_user
krb_user="${KRB5_USER:-${KRB5_USERNAME:-}}" krb_user="${KRB5_USER:-${KRB5_USERNAME:-}}"
if [[ -n "${KRB5_PASSWORD:-}" && -n "${KRB5_REALM:-}" && -n "$krb_user" ]]; then if [[ -n "${KRB5_PASSWORD:-}" && -n "${KRB5_REALM:-}" && -n "$krb_user" ]]; then
@ -346,7 +425,6 @@ init_openbao_kv_and_store_admin_key() {
docker_start() { docker_start() {
echo "Starting local Docker container for OpenBao ..." echo "Starting local Docker container for OpenBao ..."
ensure_docker ensure_docker
docker rm -f "$OPENBAO_NAME" >/dev/null 2>&1 || true
# OpenBao dev # OpenBao dev
local token local token
token=$(cat "$root_token_file" 2>/dev/null || true) token=$(cat "$root_token_file" 2>/dev/null || true)
@ -355,7 +433,29 @@ docker_start() {
printf "%s" "$token" >"$root_token_file" printf "%s" "$token" >"$root_token_file"
chmod 0600 "$root_token_file" chmod 0600 "$root_token_file"
fi 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" "$OPENBAO_IMAGE" server -dev -dev-listen-address=0.0.0.0:8200 -dev-root-token-id="$token"
echo "Docker container started: $OPENBAO_NAME" echo "Docker container started: $OPENBAO_NAME"
} }
@ -392,7 +492,8 @@ cmd_status() {
ok=1 ok=1
fi 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 if kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" >/dev/null 2>&1; then
local ready desired local ready desired
ready=$(kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0") 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 ok=1
fi fi
elif kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" >/dev/null 2>&1; then elif kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" >/dev/null 2>&1; then
# Get readiness
local ready desired local ready desired
ready=$(kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0") 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") 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)" echo "[WARN] OpenBao Deployment not fully ready ($ready/$desired)"
ok=1 ok=1
fi fi
else
echo "[MISSING] OpenBao Deployment or StatefulSet '$OPENBAO_NAME' in namespace '$NAMESPACE'"
ok=1
fi fi
if kubectl -n "$NAMESPACE" get configmap prole-krb5-conf >/dev/null 2>&1; then if kubectl -n "$NAMESPACE" get configmap prole-krb5-conf >/dev/null 2>&1; then
echo "[OK] Kerberos ConfigMap 'prole-krb5-conf' present" echo "[OK] Kerberos ConfigMap 'prole-krb5-conf' present"
else fi
echo "[MISSING] Kerberos ConfigMap 'prole-krb5-conf' in namespace '$NAMESPACE'"
ok=1
fi fi
# Docker (optional local mode) # Docker (optional local mode)
@ -457,13 +551,7 @@ cmd_status() {
# OpenBao reachability and token validity (best-effort) # OpenBao reachability and token validity (best-effort)
# Prefer explicit override, then localhost port-forward, then cluster DNS # Prefer explicit override, then localhost port-forward, then cluster DNS
local svc_url local svc_url
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then svc_url="$(bao_service_url)"
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
if curl -sS "$svc_url/v1/sys/health" >/dev/null 2>&1; then if curl -sS "$svc_url/v1/sys/health" >/dev/null 2>&1; then
echo "[OK] OpenBao service reachable" echo "[OK] OpenBao service reachable"
if [[ -s "$root_token_file" ]]; then if [[ -s "$root_token_file" ]]; then
@ -523,7 +611,7 @@ case "$ACTION" in
;; ;;
initialize) initialize)
ensure_tools ensure_tools
ensure_namespace ensure_docker
# Read password from stdin if provided # Read password from stdin if provided
db_pass="" db_pass=""
if [[ ! -t 0 ]]; then if [[ ! -t 0 ]]; then
@ -531,52 +619,14 @@ case "$ACTION" in
fi fi
ensure_admin_keypair ensure_admin_keypair
generate_openbao_manifests docker_start
generate_kerberos_configmap wait_for_openbao_http 20
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
init_openbao_kv_and_store_admin_key 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) update|reload)
ensure_tools ensure_tools
ensure_k8s_tools
ensure_namespace ensure_namespace
generate_openbao_manifests generate_openbao_manifests
generate_kerberos_configmap generate_kerberos_configmap

View File

@ -82,7 +82,30 @@ parse_key_output() {
printf "%s\n%s" "$access_key" "$secret_key" 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_bucket_and_key() {
ensure_garage_ready
echo "Ensuring Garage bucket and access key for backups ..." echo "Ensuring Garage bucket and access key for backups ..."
local key_info parsed access_key secret_key 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 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 fi
ensure_tools() { 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; } command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
done done
} }
@ -113,9 +113,10 @@ start() {
exit 1 exit 1
fi fi
# 3. openbao is configured # 3. openbao is configured (local container)
if ! kubectl get statefulset openbao -n "$NAMESPACE" >/dev/null 2>&1 && ! kubectl get deployment openbao -n "$NAMESPACE" >/dev/null 2>&1; then local openbao_url="${PROLE_OPENBAO_URL:-http://127.0.0.1:18200}"
echo "ERROR: OpenBao is not deployed." >&2 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 exit 1
fi fi

View File

@ -80,6 +80,7 @@ DEFAULT_ACTION_FLAGS = {
'init_cnpg_deploy.run_deploy': True, 'init_cnpg_deploy.run_deploy': True,
'init_cnpg_deploy.force_rollout': False, 'init_cnpg_deploy.force_rollout': False,
'kerberos_config.test_connection': False, 'kerberos_config.test_connection': False,
'kerberos_config.init_authority': False,
'build.run_build': False, 'build.run_build': False,
} }
@ -456,7 +457,7 @@ def _render_prole_cfg(inputs: dict, globals_to_save: dict, sections: dict, gener
sections_order = [ sections_order = [
'Welcome', 'Dependencies', 'Network', 'System Environment', 'Welcome', 'Dependencies', 'Network', 'System Environment',
'Monitoring', 'Kerberos Authentication', 'Optional Features', 'Database Creation', 'Monitoring', 'Kerberos Authentication', 'Optional Features', 'Database Creation',
'Docker Build', 'Initialize Cluster', 'Initialization Scripts', 'Initialize Cluster', 'Docker Build', 'Initialization Scripts',
'Prole DB Deploy', 'Install' 'Prole DB Deploy', 'Install'
] ]
@ -732,8 +733,8 @@ class ProleInstaller:
("Network", "network_scan"), ("Network", "network_scan"),
("System Environment", "env_setup"), ("System Environment", "env_setup"),
("Database Creation", "init_password"), ("Database Creation", "init_password"),
("Docker Build", "init_db_build"),
("Start Cluster", "init_cluster"), ("Start Cluster", "init_cluster"),
("Docker Build", "init_db_build"),
("Initialization Scripts", "init_scripts"), ("Initialization Scripts", "init_scripts"),
("Kerberos Authentication", "kerberos_config"), ("Kerberos Authentication", "kerberos_config"),
("Supabase", "supabase_config"), ("Supabase", "supabase_config"),
@ -864,8 +865,8 @@ class ProleInstaller:
self._register_page('network_scan', None) self._register_page('network_scan', None)
self._register_page('env_setup', None) self._register_page('env_setup', None)
self._register_page('init_password', None) self._register_page('init_password', None)
self._register_page('init_db_build', None)
self._register_page('init_cluster', None) self._register_page('init_cluster', None)
self._register_page('init_db_build', None)
self._register_page('init_scripts', None) self._register_page('init_scripts', None)
self._register_page('kerberos_config', None) self._register_page('kerberos_config', None)
self._register_page('supabase_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')) self.safe_after(lambda: self._db_set_status("Updating OpenBao...", '#1d1d1f'))
env = self._script_env_for_namespace(ns) 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") 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") rc2, out2 = self._run_cmd_capture(['bash', script_path, 'initialize'], env=env, stdin_text=f"{p1}\n")
self._db_log(out2) self._db_log(out2)
@ -2134,6 +2136,7 @@ class ProleInstaller:
_set('kerberos_config.user', _get_var(self.kerberos_user, '')) _set('kerberos_config.user', _get_var(self.kerberos_user, ''))
_set('kerberos_config.password', _get_var(self.kerberos_password, '')) _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.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 # Init scripts + deploy
_set_bool('init_scripts.run_scripts', _action('init_scripts.run_scripts', DEFAULT_ACTION_FLAGS.get('init_scripts.run_scripts', True))) _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 [ sections = {k: self.prole_cfg_data.get(k, {}) for k in [
'Welcome', 'Dependencies', 'Network', 'System Environment', 'Welcome', 'Dependencies', 'Network', 'System Environment',
'Kerberos Authentication', 'Optional Features', 'Database Creation', 'Kerberos Authentication', 'Optional Features', 'Database Creation',
'Docker Build', 'Initialize Cluster', 'Initialization Scripts', 'Initialize Cluster', 'Docker Build', 'Initialization Scripts',
'Prole DB Deploy', 'Install' 'Prole DB Deploy', 'Install'
]} ]}
@ -2647,6 +2650,15 @@ class ProleInstaller:
self._canvas_items.append(btn_window) self._canvas_items.append(btn_window)
self._overlay_widgets.append(test_btn) 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 y += 60
# Use tk.Text # Use tk.Text
self.kerberos_status_text = tk.Text(self.bg_canvas, width=100, height=12, font=('Menlo', 10), 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) self._overlay_widgets.append(self.kerberos_status_text)
def _render_supabase_config_page(self): 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 content_width = self.bg_canvas.winfo_width() or 975
right_margin = content_width - 48 right_margin = content_width - 48
@ -2665,55 +2677,89 @@ class ProleInstaller:
ui.canvas_text(self, right_margin, 85, "Infrastructure Automated.", fill='#6e6e73', ui.canvas_text(self, right_margin, 85, "Infrastructure Automated.", fill='#6e6e73',
font=('SF Pro Text', 18), anchor='ne') font=('SF Pro Text', 18), anchor='ne')
self._render_title('Supabase Deployment', y=150) self._render_title('Prole::Supabase', 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)
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 y = 280
self._supabase_status_label = ui.canvas_text(self, 48, y, "Status: Ready", fill='#6e6e73', font=('SF Pro Text', 12, 'bold')) self._supabase_status_var = tk.StringVar(value="Ready")
self._canvas_items.append(self._supabase_status_label) 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, self._supabase_deploy_button = tk.Button(self.bg_canvas, text="Deploy Supabase", command=self.run_supabase_deploy,
bg='#007aff', fg='white', activebackground='#0062cc', bg='#F5F5DC', fg='black', activebackground='#E5E5D5',
highlightthickness=0, relief='flat', font=('SF Pro Text', 12, 'bold'), highlightbackground='#F5F5DC', highlightthickness=0,
padx=20, pady=10) 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') 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) self._overlay_widgets.append(self._supabase_deploy_button)
self._canvas_items.append(btn_window)
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)
def run_supabase_deploy(self): def run_supabase_deploy(self):
if getattr(self, '_supabase_deploying', False):
return
self._supabase_deploying = True
self._supabase_deploy_button.configure(state='disabled') 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() self._supabase_console.clear()
def worker(): def worker():
etc_dir = PROJECT_ROOT / "etc"
env = os.environ.copy() env = os.environ.copy()
env["PROLE_HOME"] = str(PROJECT_ROOT) 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") 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"), on_line=lambda l: self._supabase_console.write(l + "\n"),
env=env) env=env)
if proc.returncode == 0: if proc.returncode == 0:
self._supabase_success = True 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") self._supabase_console.write("\nSupabase deployment completed successfully.\n")
else: else:
self._supabase_success = False 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_console.write(f"\nSupabase deployment failed with exit code {proc.returncode}.\n")
self._supabase_deploying = False
self._supabase_deploy_button.configure(state='normal') self._supabase_deploy_button.configure(state='normal')
self.update_footer() self.update_footer()
@ -2748,7 +2794,7 @@ class ProleInstaller:
pass pass
self.kerberos_status_text.delete('1.0', tk.END) 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(): def worker():
try: try:
@ -2775,19 +2821,6 @@ class ProleInstaller:
pass pass
self.safe_after(_do_write) 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") write_line("\n==> init_kerberos.sh test\n")
rc2 = self.controller.run_script( rc2 = self.controller.run_script(
"init_kerberos.sh", "init_kerberos.sh",
@ -2811,6 +2844,88 @@ class ProleInstaller:
threading.Thread(target=worker, daemon=True).start() 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): def _start_welcome_dependency_scan(self):
self.splash_scan_running = True self.splash_scan_running = True
self.splash_scan_done_at = None self.splash_scan_done_at = None
@ -3077,6 +3192,8 @@ class ProleInstaller:
def worker(): def worker():
key_path = Path.home() / ".ssh" / "id_prole_ed25519" key_path = Path.home() / ".ssh" / "id_prole_ed25519"
key_path.parent.mkdir(parents=True, exist_ok=True) 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 # Pre-pull dependent images into local registry before initialization
try: try:
@ -3093,8 +3210,8 @@ class ProleInstaller:
if key_path.exists(): if key_path.exists():
console.write(f"Key already exists at {key_path}. Proceeding...\n") 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) 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(1.5) time.sleep(0.5)
else: else:
self.safe_after(lambda: self.bg_canvas.itemconfig(status_label, text="Preparing keys...", fill='blue') if self.bg_canvas.winfo_exists() else None) 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()] 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) 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 # In case of error, we might want to let the user see it before continuing or stopping
time.sleep(2) 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 # 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() threading.Thread(target=worker, daemon=True).start()
@ -3144,7 +3285,7 @@ class ProleInstaller:
font=('SF Pro Text', 18), anchor='ne') font=('SF Pro Text', 18), anchor='ne')
self._render_title('Start Cluster', y=150) 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) # Cluster Selection (Radio Buttons)
x_label = 48 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.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) 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 y += 50
# Use tk.Button on canvas # Use tk.Button on canvas
btn = tk.Button(self.bg_canvas, text='Start Cluster', command=self.ensure_cluster_ready, 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_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' 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(): def update_ui():
if hasattr(self, 'docker_status_label'): if hasattr(self, 'docker_status_label'):
self.bg_canvas.itemconfig(self.docker_status_label, text=docker_msg, fill=docker_fill) self.bg_canvas.itemconfig(self.docker_status_label, text=docker_msg, fill=docker_fill)
if hasattr(self, 'k3d_status_label'): if hasattr(self, 'k3d_status_label'):
self.bg_canvas.itemconfig(self.k3d_status_label, text=k3d_msg, fill=k3d_fill) 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'): if hasattr(self, '_init_cluster_button'):
label = 'Verify Cluster' if k3d_ok else 'Start Cluster' label = 'Verify Cluster' if k3d_ok else 'Start Cluster'
try: try:
@ -3237,7 +3414,26 @@ class ProleInstaller:
self.root.after(0, lambda: messagebox.showerror('Docker', 'Could not start Docker. Please start it manually.')) self.root.after(0, lambda: messagebox.showerror('Docker', 'Could not start Docker. Please start it manually.'))
return 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" cluster_name = f"prole-{self.cluster_env.get()}-cluster"
res = subprocess.run(['k3d', 'cluster', 'list', '--no-headers'], capture_output=True, text=True) res = subprocess.run(['k3d', 'cluster', 'list', '--no-headers'], capture_output=True, text=True)
if cluster_name not in res.stdout: if cluster_name not in res.stdout:
@ -3741,13 +3937,11 @@ class ProleInstaller:
def _init_scripts_list(self): def _init_scripts_list(self):
scripts = [ scripts = [
('Kubectl Status', self.KUBECTL_STATUS_TAB), ('Kubectl Status', self.KUBECTL_STATUS_TAB),
('OpenBao', 'init_openbao.sh'),
('Garage Store', 'init_garage_store.sh'), ('Garage Store', 'init_garage_store.sh'),
('Garage Log', 'garage_log'), ('Garage Log', 'garage_log'),
('CloudNative-PG', 'init_cloudnative_pg.sh'), ('CloudNative-PG', 'init_cloudnative_pg.sh'),
] ]
if self.kerberos_enabled.get(): if self.kerberos_enabled.get():
scripts.append(('Authority', 'init_authority.sh'))
scripts.append(('Kerberos Realm', 'init_kerberos.sh')) scripts.append(('Kerberos Realm', 'init_kerberos.sh'))
scripts.extend([ scripts.extend([
('Prole DB', 'init_prole-db.sh'), ('Prole DB', 'init_prole-db.sh'),
@ -3767,7 +3961,7 @@ class ProleInstaller:
font=('SF Pro Text', 18), anchor='ne') font=('SF Pro Text', 18), anchor='ne')
self._render_title('Initialization Scripts', y=150) 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 # Tabs for output - using standardized appearance
ui.canvas_text(self, 48, 260, "Execution Output", fill='#1d1d1f', font=('SF Pro Text', 12, 'bold')) 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 / 'init_garage_store.log'
return logs_dir / f'{base}.log' return logs_dir / f'{base}.log'
# 1. init_openbao.sh initialize # 0. Pre-emptive stop for port-forwards to avoid conflicts on re-install
script = "init_openbao.sh" script_pf = "init_port_forwards.sh"
_select_tab(script) if script_pf in self.script_consoles:
self.script_consoles[script].clear() self.script_consoles[script_pf].write("Stopping existing port-forwards to avoid conflicts...\n")
self.script_consoles[script].write(f"Running {script} initialize...\n") self.controller.run_script(
log_path = _log_path_for(script) script_pf,
self._record_install_log(log_path) args=['stop'],
try: env=env
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
) )
if log_fp:
try:
log_fp.close()
except Exception:
pass
# 2. init_garage_store.sh start # 1. init_garage_store.sh start
overall_success = (rc1 == 0) overall_success = True
if overall_success: if overall_success:
script = "init_garage_store.sh" script = "init_garage_store.sh"
_select_tab(script) _select_tab(script)
@ -4061,9 +4231,9 @@ class ProleInstaller:
else: else:
self.script_consoles[script].write(f"\n{script} completed successfully.\n") self.script_consoles[script].write(f"\n{script} completed successfully.\n")
else: 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: if overall_success:
script = "init_cloudnative_pg.sh" script = "init_cloudnative_pg.sh"
_select_tab(script) _select_tab(script)
@ -4108,49 +4278,7 @@ class ProleInstaller:
if "init_cloudnative_pg.sh" in self.script_consoles: 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") self.script_consoles["init_cloudnative_pg.sh"].write("Skipping CloudNative-PG initialization because previous steps failed.\n")
# 4. init_authority.sh start (optional) # 3. init_kerberos.sh initialize (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)
if overall_success and self.kerberos_enabled.get(): if overall_success and self.kerberos_enabled.get():
script = "init_kerberos.sh" script = "init_kerberos.sh"
if script in self.script_consoles: if script in self.script_consoles:
@ -4722,14 +4850,14 @@ class ProleInstaller:
if current_id == 'init_password': if current_id == 'init_password':
self.show_page('env_setup') self.show_page('env_setup')
return return
if current_id == 'init_db_build': if current_id == 'init_cluster':
self.show_page('init_password') self.show_page('init_password')
return return
if current_id == 'init_cluster': if current_id == 'init_db_build':
self.show_page('init_db_build') self.show_page('init_cluster')
return return
if current_id == 'init_scripts': if current_id == 'init_scripts':
self.show_page('init_cluster') self.show_page('init_db_build')
return return
if current_id == 'kerberos_config': if current_id == 'kerberos_config':
self.show_page('init_scripts') self.show_page('init_scripts')
@ -4847,6 +4975,24 @@ class ProleInstaller:
self.show_page('init_password') self.show_page('init_password')
return 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': if current_id == 'init_cluster':
# Capture cluster info # Capture cluster info
self.prole_cfg_data['Initialize Cluster']['ENVIRONMENT'] = self.cluster_env.get() self.prole_cfg_data['Initialize Cluster']['ENVIRONMENT'] = self.cluster_env.get()
@ -4861,7 +5007,7 @@ class ProleInstaller:
except Exception: except Exception:
pass pass
return return
self.show_page('init_scripts') self.show_page('init_db_build')
return return
if current_id == 'init_scripts': if current_id == 'init_scripts':
@ -7764,6 +7910,7 @@ class ProleSilentInstaller:
inputs['kerberos_config.user'] = '' inputs['kerberos_config.user'] = ''
inputs['kerberos_config.password'] = '' inputs['kerberos_config.password'] = ''
inputs['kerberos_config.test_connection'] = _bool_str(DEFAULT_ACTION_FLAGS.get('kerberos_config.test_connection', False)) 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 # Init scripts + deploy
inputs['init_scripts.run_scripts'] = _bool_str(DEFAULT_ACTION_FLAGS.get('init_scripts.run_scripts', True)) 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 [ sections = {k: self.prole_cfg_data.get(k, {}) for k in [
'Welcome', 'Dependencies', 'Network', 'System Environment', 'Welcome', 'Dependencies', 'Network', 'System Environment',
'Kerberos Authentication', 'Optional Features', 'Database Creation', 'Kerberos Authentication', 'Optional Features', 'Database Creation',
'Docker Build', 'Initialize Cluster', 'Initialization Scripts', 'Initialize Cluster', 'Docker Build', 'Initialization Scripts',
'Prole DB Deploy', 'Install' 'Prole DB Deploy', 'Install'
]} ]}
cfg_text = _render_prole_cfg(self.inputs, globals_to_save, sections) 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)): 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) 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): def _generate_ssh_key(self, user: str):
self.log("==> Environment Preparation (Secure Access)") self.log("==> Environment Preparation (Secure Access)")
key_path = Path.home() / ".ssh" / "id_prole_ed25519" key_path = Path.home() / ".ssh" / "id_prole_ed25519"
@ -8482,6 +8668,21 @@ class ProleSilentInstaller:
if not self.controller.check_docker_running(): if not self.controller.check_docker_running():
raise Exception("Docker is not 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" cluster_name = f"prole-{cluster_env}-cluster"
res = subprocess.run(['k3d', 'cluster', 'list', '--no-headers'], capture_output=True, text=True) res = subprocess.run(['k3d', 'cluster', 'list', '--no-headers'], capture_output=True, text=True)
if cluster_name not in res.stdout: 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_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') 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 = os.environ.copy()
env["PROLE_HOME"] = str(self.project_root) env["PROLE_HOME"] = str(self.project_root)
env["PROLE_SERVICE"] = str(self.project_root) env["PROLE_SERVICE"] = str(self.project_root)
@ -8543,11 +8737,25 @@ class ProleSilentInstaller:
env["KRB5_PASSWORD"] = password env["KRB5_PASSWORD"] = password
env.setdefault("KRB5_AD_PORT_FORWARD", "1") env.setdefault("KRB5_AD_PORT_FORWARD", "1")
self.log("==> Kerberos test: init_openbao.sh initialize") if self._get_input_bool('kerberos_config.init_authority', False):
rc1 = self._run_script("init_openbao.sh", args=["initialize"], env=env, stdin_text=f"{self._get_input('init_password.db_password', '')}\n") self.log("==> Kerberos authority: init_authority.sh start")
if rc1 != 0: rc_auth_bao = self._run_script("init_openbao.sh", args=["start"], env=env)
self.err(f"[ERROR] OpenBao initialization failed (code {rc1})") 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 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") self.log("==> Kerberos test: init_kerberos.sh test")
rc2 = self._run_script("init_kerberos.sh", args=["test"], env=env) rc2 = self._run_script("init_kerberos.sh", args=["test"], env=env)
if rc2 != 0: if rc2 != 0:
@ -8586,8 +8794,10 @@ class ProleSilentInstaller:
env["KRB5_PASSWORD"] = krb_pw env["KRB5_PASSWORD"] = krb_pw
env.setdefault("KRB5_AD_PORT_FORWARD", "1") 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 = [ steps = [
("init_openbao.sh", ["initialize"], True),
("init_garage_store.sh", ["start"], False), ("init_garage_store.sh", ["start"], False),
("init_cloudnative_pg.sh", ["initialize"], False), ("init_cloudnative_pg.sh", ["initialize"], False),
] ]
@ -8659,7 +8869,7 @@ class ProleSilentInstaller:
env["PROLE_HOME"] = str(self.project_root) env["PROLE_HOME"] = str(self.project_root)
env["PROLE_SERVICE"] = str(self.project_root) env["PROLE_SERVICE"] = str(self.project_root)
env["NAMESPACE"] = (self._get_input('init_password.db_namespace', '') or '').strip() 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: if rc == 0:
self._supabase_success = True self._supabase_success = True
self.prole_cfg_data['Supabase'] = {'STATUS': 'Deployed'} self.prole_cfg_data['Supabase'] = {'STATUS': 'Deployed'}
@ -8694,10 +8904,10 @@ class ProleSilentInstaller:
self._step_init_password() self._step_init_password()
self._write_cfg() self._write_cfg()
self._step_db_build() self._step_cluster()
self._write_cfg() self._write_cfg()
self._step_cluster() self._step_db_build()
self._write_cfg() self._write_cfg()
self._step_init_scripts() self._step_init_scripts()

View File

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