diff --git a/.idea/runConfigurations/Silent_Install_Test.xml b/.idea/runConfigurations/Silent_Install_Test.xml new file mode 100644 index 0000000..4a2608c --- /dev/null +++ b/.idea/runConfigurations/Silent_Install_Test.xml @@ -0,0 +1,19 @@ + + + + diff --git a/conf/port-mappings.properties b/conf/port-mappings.properties index 3d0f7ad..ba6bc7a 100644 --- a/conf/port-mappings.properties +++ b/conf/port-mappings.properties @@ -2,9 +2,8 @@ - + - diff --git a/env.sh b/env.sh index 6e5eaab..f0eeeb3 100755 --- a/env.sh +++ b/env.sh @@ -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; } diff --git a/etc/init_cloudnative_pg.sh b/etc/init_cloudnative_pg.sh index d50d223..f719633 100755 --- a/etc/init_cloudnative_pg.sh +++ b/etc/init_cloudnative_pg.sh @@ -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." diff --git a/etc/init_garage_store.sh b/etc/init_garage_store.sh index ef06bca..ee7f4f8 100755 --- a/etc/init_garage_store.sh +++ b/etc/init_garage_store.sh @@ -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 diff --git a/etc/init_kerberos_test.sh b/etc/init_kerberos_test.sh index 81aa88d..cce7c20 100755 --- a/etc/init_kerberos_test.sh +++ b/etc/init_kerberos_test.sh @@ -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 } diff --git a/etc/init_monitoring.sh b/etc/init_monitoring.sh new file mode 100755 index 0000000..05b550d --- /dev/null +++ b/etc/init_monitoring.sh @@ -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 diff --git a/etc/init_openbao.sh b/etc/init_openbao.sh index 523399e..addfe70 100755 --- a/etc/init_openbao.sh +++ b/etc/init_openbao.sh @@ -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,42 +492,36 @@ cmd_status() { ok=1 fi - # K8s resources - 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") - desired=$(kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" -o jsonpath='{.status.replicas}' 2>/dev/null || echo "0") - ready=${ready:-0} - desired=${desired:-0} - if [[ "$ready" == "$desired" && "$ready" != "0" ]]; then - echo "[OK] OpenBao StatefulSet running ($ready/$desired ready)" - else - echo "[WARN] OpenBao StatefulSet not fully ready ($ready/$desired)" - ok=1 + # 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") + desired=$(kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" -o jsonpath='{.status.replicas}' 2>/dev/null || echo "0") + ready=${ready:-0} + desired=${desired:-0} + if [[ "$ready" == "$desired" && "$ready" != "0" ]]; then + echo "[OK] OpenBao StatefulSet running ($ready/$desired ready)" + else + echo "[WARN] OpenBao StatefulSet not fully ready ($ready/$desired)" + ok=1 + fi + elif kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" >/dev/null 2>&1; then + 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") + ready=${ready:-0} + desired=${desired:-0} + if [[ "$ready" == "$desired" && "$ready" != "0" ]]; then + echo "[OK] OpenBao Deployment running ($ready/$desired ready)" + else + echo "[WARN] OpenBao Deployment not fully ready ($ready/$desired)" + ok=1 + fi 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") - ready=${ready:-0} - desired=${desired:-0} - if [[ "$ready" == "$desired" && "$ready" != "0" ]]; then - echo "[OK] OpenBao Deployment running ($ready/$desired ready)" - else - echo "[WARN] OpenBao Deployment not fully ready ($ready/$desired)" - ok=1 + if kubectl -n "$NAMESPACE" get configmap prole-krb5-conf >/dev/null 2>&1; then + echo "[OK] Kerberos ConfigMap 'prole-krb5-conf' present" 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 # 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 diff --git a/etc/init_prole-db-backup.sh b/etc/init_prole-db-backup.sh index ab141af..88c2cd3 100755 --- a/etc/init_prole-db-backup.sh +++ b/etc/init_prole-db-backup.sh @@ -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 diff --git a/etc/init_prole-db.sh b/etc/init_prole-db.sh index 5600aa1..8b8e611 100755 --- a/etc/init_prole-db.sh +++ b/etc/init_prole-db.sh @@ -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 diff --git a/install.py b/install.py index 63516e2..2b43650 100755 --- a/install.py +++ b/install.py @@ -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)) + + 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) - y += 40 + # 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, @@ -3202,12 +3351,40 @@ class ProleInstaller: k3d_ok = cluster_name in res.stdout 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 + # 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 + ) - 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 - 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') @@ -4846,6 +4974,24 @@ class ProleInstaller: return 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 @@ -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() diff --git a/installer/ncurses_installer.py b/installer/ncurses_installer.py index 18e2176..8e79132 100644 --- a/installer/ncurses_installer.py +++ b/installer/ncurses_installer.py @@ -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"), ] diff --git a/supabase.sh b/supabase.sh new file mode 120000 index 0000000..5852443 --- /dev/null +++ b/supabase.sh @@ -0,0 +1 @@ +supabase/deploy.sh \ No newline at end of file diff --git a/tests/silent_install_test.sh b/tests/silent_install_test.sh new file mode 100755 index 0000000..d2c0477 --- /dev/null +++ b/tests/silent_install_test.sh @@ -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\s*)(?P[^=]+?)(?P
\s*)=(?P\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 ""