diff --git a/README.md b/README.md
index 1fda3d9..7d109ed 100644
--- a/README.md
+++ b/README.md
@@ -32,10 +32,10 @@ k3d cluster create prole-dev-cluster -a 2 --api-port 0.0.0.0:6443
# build prole-db Docker image
```commandline
cd prole-db
-docker build -t prole-db:17.5-027 .
-k3d image import prole-db:17.5-027 -c prole-service-cluster
-docker tag prole-db:17.5-027 k8s.prole.org:5000/prole-db:17.5-027
-docker push k8s.prole.org:5000/prole-db:17.5-027
+docker build -t prole-db:17.7-031 .
+k3d image import prole-db:17.7-031 -c prole-service-cluster
+docker tag prole-db:17.7-031 k8s.prole.org:5000/prole-db:17.7-031
+docker push k8s.prole.org:5000/prole-db:17.7-031
# vim ~/.docker/daemon.json
# "insecure-registries": [ "k3.localhost:5000", "k8s.prole.org:5000" ]
```
diff --git a/docker-root-prole-db.sh b/docker-root-prole-db.sh
index c543eec..f6e894a 100755
--- a/docker-root-prole-db.sh
+++ b/docker-root-prole-db.sh
@@ -12,4 +12,4 @@ docker run --rm -it -u root \
--network=bridge \
--entrypoint bash \
--restart=no \
-prole-db:17.5-025
+prole-db:17.7-031
diff --git a/etc/init_cloudnative_pg.sh b/etc/init_cloudnative_pg.sh
index e11502f..8357f6b 100755
--- a/etc/init_cloudnative_pg.sh
+++ b/etc/init_cloudnative_pg.sh
@@ -78,7 +78,7 @@ bao_service_url() {
echo "http://$OPENBAO_NAME.$NAMESPACE.svc.cluster.local:8200"
}
-fetch_admin_keys_from_bao_or_local() {
+fetch_admin_keys_and_db_pass_from_bao_or_local() {
local token url
if [[ -f "$OPENBAO_TOKEN_FILE" ]]; then
token=$(cat "$OPENBAO_TOKEN_FILE")
@@ -95,9 +95,28 @@ fetch_admin_keys_from_bao_or_local() {
printf "%s" "$priv_b64" | base64 -d >"$ADMIN_PRIV"
printf "%s" "$pub_b64" | base64 -d >"$ADMIN_PUB"
chmod 0600 "$ADMIN_PRIV"
- return 0
+ fi
+
+ echo "Attempting to read database password from OpenBao kv/prole/db ..."
+ if curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/db" | jq -e '.data.data' >/dev/null 2>&1; then
+ local db_pass
+ db_pass=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/db" | jq -r '.data.data.password')
+ if [[ -n "$db_pass" ]]; then
+ echo "Updating database user secret 'prole-db-user' from OpenBao ..."
+ kubectl create secret generic prole-db-user -n "$NAMESPACE" \
+ --from-literal=username=prole \
+ --from-literal=password="$db_pass" \
+ --dry-run=client -o yaml | kubectl apply -f -
+
+ echo "Updating database superuser secret 'prole-db-superuser' from OpenBao ..."
+ 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 -f -
+ fi
fi
fi
+
if [[ -f "$ADMIN_PRIV" && -f "$ADMIN_PUB" ]]; then
echo "Using local admin key pair at $SECRETS_DIR"
return 0
@@ -177,6 +196,7 @@ patch_cnpg_cluster_for_auth() {
\"parameters\": {\"krb_srvname\": null},
\"pg_hba\": [
\"local all postgres trust\",
+ \"host all postgres all scram-sha-256\",
\"host all all all gss include_realm=1 krb_realm=$REALM\",
\"host all all all scram-sha-256\"
]
@@ -196,7 +216,7 @@ initialize() {
echo "Ensuring port-forward for OpenBao is active ..."
"$SCRIPT_DIR/init_port_forwards.sh" restart openbao
- fetch_admin_keys_from_bao_or_local
+ fetch_admin_keys_and_db_pass_from_bao_or_local
apply_cnpg_admin_secret
ensure_krb5_conf_configmap
# generate_tls_if_missing
@@ -225,7 +245,26 @@ case "$ACTION" in
kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.27/releases/cnpg-1.27.0.yaml
echo "Creating CloudNative-PG cluster and resources for '$CNPG_CLUSTER_NAME' ..."
+
kubectl apply -k "$SCRIPT_DIR/../k8s/prole"
+
+ # Ensure prole-index-html exists for prole deployment readiness probe
+ if ! kubectl get configmap prole-index-html -n prole >/dev/null 2>&1; then
+ echo "Creating prole-index-html configmap..."
+ printf "
Prole
" > /tmp/index.html
+ kubectl create configmap prole-index-html --from-file=/tmp/index.html -n prole
+ rm /tmp/index.html
+ fi
+
+ # Ensure prole-nginx-tls exists (self-signed for dev)
+ if ! kubectl get secret prole-nginx-tls -n prole >/dev/null 2>&1; then
+ echo "Generating self-signed prole-nginx-tls for development..."
+ openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
+ -keyout /tmp/nginx-tls.key -out /tmp/nginx-tls.crt \
+ -subj "/CN=prole.org" >/dev/null 2>&1
+ kubectl create secret tls prole-nginx-tls --key /tmp/nginx-tls.key --cert /tmp/nginx-tls.crt -n prole
+ rm /tmp/nginx-tls.key /tmp/nginx-tls.crt
+ fi
echo "Initializing and patching cluster ..."
initialize
diff --git a/etc/init_openbao.sh b/etc/init_openbao.sh
new file mode 100755
index 0000000..a2502a0
--- /dev/null
+++ b/etc/init_openbao.sh
@@ -0,0 +1,453 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+# init_openbao.sh
+# Purpose:
+# - Deploy OpenBao to Kubernetes (dev mode) and store admin ed25519 key pair
+# Generate and apply a Kerberos krb5.conf ConfigMap for an external realm
+# - Local Docker helpers for OpenBao (optional)
+
+# Initialize SCRIPT_DIR
+SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+
+# Preserve positional args while sourcing env
+__PROLE_SAVED_ARGS=("$@")
+if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
+ set --
+ # shellcheck disable=SC1090
+ source "$PROLE_HOME/env.sh"
+elif [[ -f "$HOME/.prole/env.sh" ]]; then
+ set --
+ # shellcheck disable=SC1090
+ source "$HOME/.prole/env.sh"
+fi
+set -- "${__PROLE_SAVED_ARGS[@]}"
+unset __PROLE_SAVED_ARGS
+
+if [[ -z "${PROLE_SERVICE:-}" ]]; then
+ echo "ERROR: PROLE_SERVICE is not defined in env. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2
+ exit 1
+fi
+
+ACTION=${1:-}
+
+# Defaults
+NAMESPACE=${NAMESPACE:-default}
+OPENBAO_NAME=${OPENBAO_NAME:-openbao}
+OPENBAO_IMAGE=${OPENBAO_IMAGE:-ghcr.io/openbao/openbao:latest}
+OPENBAO_MANIFEST_DIR="$SCRIPT_DIR/../k8s/openbao"
+
+# Kerberos realm defaults
+KRB5_REALM=${KRB5_REALM:-PROLE.ORG}
+DOMAIN=${DOMAIN:-$(echo "$KRB5_REALM" | tr 'A-Z' 'a-z')}
+KRB5_KDC=${KRB5_KDC:-kdc.$DOMAIN}
+KRB5_ADMIN=${KRB5_ADMIN:-}
+
+# Secrets and token locations
+SECRETS_DIR="$PROLE_SERVICE/secrets"
+mkdir -p "$SECRETS_DIR"
+admin_key_priv="$SECRETS_DIR/admin_ed25519.key"
+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
+ command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
+ done
+}
+
+ensure_docker() {
+ command -v docker >/dev/null || { echo "Missing required tool: docker" >&2; exit 1; }
+}
+
+ensure_admin_keypair() {
+ # Ensure admin ed25519 keypair exists and a root token is available
+ mkdir -p "$SECRETS_DIR"
+ if [[ ! -f "$admin_key_priv" || ! -f "$admin_key_pub" ]]; then
+ echo "Generating admin ed25519 keypair in $SECRETS_DIR ..."
+ openssl genpkey -algorithm ED25519 -out "$admin_key_priv"
+ openssl pkey -in "$admin_key_priv" -pubout -out "$admin_key_pub"
+ chmod 0600 "$admin_key_priv"
+ fi
+ # Ensure a root token exists for OpenBao dev server interactions
+ if [[ ! -f "$root_token_file" || ! -s "$root_token_file" ]]; then
+ openssl rand -hex 24 >"$root_token_file"
+ chmod 0600 "$root_token_file"
+ fi
+}
+
+generate_openbao_manifests() {
+ mkdir -p "$OPENBAO_MANIFEST_DIR"
+ # Only create a simple deployment if none exists; otherwise respect current file
+ local deploy="$OPENBAO_MANIFEST_DIR/deployment.yaml"
+ if [[ ! -f "$deploy" ]]; then
+ cat >"$deploy" <"$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml" </dev/null 2>&1 || true
+ fi
+
+ local priv pub
+ priv=$(base64 <"$admin_key_priv" | tr -d '\n')
+ pub=$(base64 <"$admin_key_pub" | tr -d '\n')
+
+ # Check if admin key already exists and matches
+ local existing_data
+ existing_data=$(curl -sS -H "X-Vault-Token: $token" "$svc/v1/kv/data/prole/admin" 2>/dev/null || true)
+ if [[ -n "$existing_data" ]]; then
+ local ex_priv ex_pub
+ ex_priv=$(echo "$existing_data" | grep -o '"admin_private_key_b64":"[^"]*' | cut -d'"' -f4 || true)
+ ex_pub=$(echo "$existing_data" | grep -o '"admin_public_key_b64":"[^"]*' | cut -d'"' -f4 || true)
+
+ if [[ "$ex_priv" == "$priv" && "$ex_pub" == "$pub" ]]; then
+ echo "Admin key pair in OpenBao kv/prole/admin is already up to date."
+ else
+ echo "Writing admin key pair to kv/prole/admin ..."
+ curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
+ -X POST "$svc/v1/kv/data/prole/admin" \
+ -d "{\"data\":{\"admin_private_key_b64\":\"$priv\",\"admin_public_key_b64\":\"$pub\"}}" >/dev/null
+ echo "Stored admin key pair in OpenBao kv/prole/admin."
+ fi
+ else
+ echo "Writing admin key pair to kv/prole/admin ..."
+ curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
+ -X POST "$svc/v1/kv/data/prole/admin" \
+ -d "{\"data\":{\"admin_private_key_b64\":\"$priv\",\"admin_public_key_b64\":\"$pub\"}}" >/dev/null
+ echo "Stored admin key pair in OpenBao kv/prole/admin."
+ fi
+
+ if [[ -n "$db_pass" ]]; then
+ echo "Writing database user password to kv/prole/db ..."
+ curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
+ -X POST "$svc/v1/kv/data/prole/db" \
+ -d "{\"data\":{\"username\":\"prole\",\"password\":\"$db_pass\"}}" >/dev/null
+ echo "Stored database password in OpenBao kv/prole/db."
+ fi
+}
+
+# Removed: prompt_admin_password_and_apply_secret (no AD admin secret required)
+
+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)
+ if [[ -z "$token" ]]; then
+ token=$(openssl rand -hex 24)
+ printf "%s" "$token" >"$root_token_file"
+ chmod 0600 "$root_token_file"
+ fi
+ docker run -d --name "$OPENBAO_NAME" -p 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"
+}
+
+docker_stop() {
+ docker rm -f "$OPENBAO_NAME" >/dev/null 2>&1 || true
+ echo "Stopped OpenBao container if it was running."
+}
+
+docker_restart() {
+ docker_stop
+ docker_start
+}
+
+# status command implementation wrapped in a function to avoid top-level 'local'
+cmd_status() {
+ echo "--- init_primary_domain status ---"
+ echo "Namespace: $NAMESPACE"
+ echo "OpenBao name: $OPENBAO_NAME"
+ echo "Manifest dir: $OPENBAO_MANIFEST_DIR"
+
+ # Files/manifests present
+ local ok=0
+ if [[ -f "$OPENBAO_MANIFEST_DIR/deployment.yaml" ]]; then
+ echo "[OK] OpenBao deployment manifest exists"
+ else
+ echo "[MISSING] $OPENBAO_MANIFEST_DIR/deployment.yaml"
+ ok=1
+ fi
+ if [[ -f "$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml" ]]; then
+ echo "[OK] Kerberos ConfigMap manifest exists"
+ else
+ echo "[MISSING] $OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml"
+ ok=1
+ fi
+
+ # K8s resources
+ if 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
+ fi
+ else
+ echo "[MISSING] OpenBao Deployment '$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)
+ if command -v docker >/dev/null 2>&1; then
+ if docker ps --format '{{.Names}}' | grep -Fxq "$OPENBAO_NAME"; then
+ echo "[OK] Docker container '$OPENBAO_NAME' is running"
+ else
+ # Not necessarily an error; we primarily use k8s
+ echo "[INFO] Docker container '$OPENBAO_NAME' not running (k8s mode may be in use)"
+ fi
+ fi
+
+ # Secrets and tokens
+ if [[ -f "$admin_key_priv" && -f "$admin_key_pub" ]]; then
+ echo "[OK] Admin ed25519 keypair present in $SECRETS_DIR"
+ else
+ echo "[MISSING] Admin keypair files in $SECRETS_DIR"
+ ok=1
+ fi
+ if [[ -s "$root_token_file" ]]; then
+ echo "[OK] OpenBao root token file present"
+ else
+ echo "[MISSING] OpenBao root token file ($root_token_file)"
+ ok=1
+ fi
+
+ # 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
+ if curl -sS "$svc_url/v1/sys/health" >/dev/null 2>&1; then
+ echo "[OK] OpenBao service reachable"
+ if [[ -s "$root_token_file" ]]; then
+ local code
+ code=$(curl -s -o /dev/null -w "%{http_code}" -H "X-Vault-Token: $(cat "$root_token_file")" "$svc_url/v1/sys/mounts" || echo "000")
+ if [[ "$code" == "200" ]]; then
+ echo "[OK] OpenBao root token authenticates"
+ else
+ echo "[WARN] OpenBao root token did not authenticate (HTTP $code)"
+ ok=1
+ fi
+ fi
+ else
+ echo "[WARN] OpenBao service not reachable at $svc_url"
+ ok=1
+ fi
+
+ # Service certificate existence (CNPG TLS)
+ local CNPG_CLUSTER_NAME
+ CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-prole-db}
+ if kubectl -n "$NAMESPACE" get secret "${CNPG_CLUSTER_NAME}-tls" >/dev/null 2>&1; then
+ echo "[OK] Service certificate secret '${CNPG_CLUSTER_NAME}-tls' exists"
+ else
+ echo "[INFO] Service certificate secret '${CNPG_CLUSTER_NAME}-tls' not found"
+ fi
+
+ echo "-------------------------------------"
+ if [[ $ok -eq 0 ]]; then
+ echo "Status: OK"
+ return 0
+ else
+ echo "Status: Issues detected"
+ return 1
+ fi
+}
+
+case "$ACTION" in
+ start)
+ ensure_tools
+ ensure_docker
+ ensure_admin_keypair
+ docker_start
+ ;;
+ status)
+ ensure_tools
+ cmd_status
+ ;;
+ stop)
+ ensure_docker
+ docker_stop
+ ;;
+ restart)
+ ensure_tools
+ ensure_docker
+ ensure_admin_keypair
+ docker_restart
+ ;;
+ initialize)
+ ensure_tools
+ # Read password from stdin if provided
+ db_pass=""
+ if [[ ! -t 0 ]]; then
+ read -r db_pass
+ fi
+
+ ensure_admin_keypair
+ generate_openbao_manifests
+ generate_kerberos_configmap
+ apply_k8s
+
+ if [[ -n "$db_pass" ]]; then
+ echo "Creating database user secret 'prole-db-user' ..."
+ kubectl create secret generic prole-db-user -n "$NAMESPACE" \
+ --from-literal=username=prole \
+ --from-literal=password="$db_pass" \
+ --dry-run=client -o yaml | kubectl apply -f -
+
+ echo "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 -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" restart openbao
+
+ if ! curl -sS http://127.0.0.1:18200/v1/sys/health >/dev/null 2>&1; then
+ echo "ERROR: OpenBao not reachable at http://127.0.0.1:18200." >&2
+ echo "Please start port-forwards: $SCRIPT_DIR/init_port_forwards.sh start openbao" >&2
+ exit 1
+ fi
+ init_openbao_kv_and_store_admin_key
+ echo "Initialization complete. Manifests in: $OPENBAO_MANIFEST_DIR"
+ ;;
+ update|reload)
+ generate_openbao_manifests
+ generate_kerberos_configmap
+ kubectl apply -f "$OPENBAO_MANIFEST_DIR/deployment.yaml"
+ kubectl apply -f "$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml"
+ echo "Re-applied manifests."
+ ;;
+ *)
+ echo "Usage: $0 {start|stop|status|restart|initialize|update|reload}" >&2
+ exit 2
+ ;;
+esac
diff --git a/install.py b/install.py
index bbd43ce..f332ef5 100755
--- a/install.py
+++ b/install.py
@@ -1110,6 +1110,26 @@ class ProleInstaller:
p2.place(x=x_field, y=y-12, width=300)
self._overlay_widgets.append(p2)
+ # Indicator for password match (X or ✓)
+ self.password_indicator = ui.canvas_text(self, x_field + 310, y, '✘', fill='#dc3545', font=('Helvetica', 16, 'bold'), state='hidden')
+ self._canvas_items.append(self.password_indicator)
+
+ def on_password_change(*args):
+ p = self.db_password.get()
+ c = self.db_password_confirm.get()
+
+ if not p:
+ self.bg_canvas.itemconfigure(self.password_indicator, state='hidden')
+ elif p == c:
+ self.bg_canvas.itemconfigure(self.password_indicator, text='✓', fill='#28a745', state='normal')
+ else:
+ self.bg_canvas.itemconfigure(self.password_indicator, text='✘', fill='#dc3545', state='normal')
+
+ self.db_password.trace_add('write', on_password_change)
+ self.db_password_confirm.trace_add('write', on_password_change)
+ # Trigger once in case they are already set
+ on_password_change()
+
def _render_init_scripts_page(self):
self._render_title('Initialization Scripts', y=40)
self._render_paragraph('Running initialization scripts to set up OpenBao, CloudNative-PG, and Port Forwards.', y=80)
@@ -3055,6 +3075,46 @@ exec "$DIR/ProleTools.bin" "$@"
"""Start the deployment process"""
threading.Thread(target=self.run_deployment, daemon=True).start()
+ def generate_prole_properties(self, env: str):
+ """Dynamically generate prole-tools-app/prole.properties based on environment"""
+ props_path = Path("prole-tools-app/prole.properties")
+
+ # Determine host based on environment
+ # For now use localhost as a placeholder for Service/Prod
+ host = "localhost"
+
+ content = f"""# Prole default endpoints (dynamically generated by install.py)
+# UI assets
+icon=img/proleIcon.png
+background=img/proleLogoSepia.png
+
+# Dev port-forward supervision
+pf.enabled=true
+
+# Service endpoints (5 traffic lights)
+svc.1.name=K3D
+svc.1.host={host}
+svc.1.port=6443
+
+svc.2.name=Prometheus
+svc.2.host={host}
+svc.2.port=9090
+
+svc.3.name=Grafana
+svc.3.host={host}
+svc.3.port=3000
+
+svc.4.name=OpenBAO
+svc.4.host={host}
+svc.4.port=8200
+
+svc.5.name=PostgreSQL
+svc.5.host={host}
+svc.5.port=5432
+"""
+ props_path.write_text(content)
+ print(f"Generated {props_path} for {env} environment")
+
def run_deployment(self):
"""Run the deployment steps"""
try:
@@ -3062,11 +3122,16 @@ exec "$DIR/ProleTools.bin" "$@"
env = self.deploy_environment.get().strip()
if env not in ('Dev', 'Service', 'Prod'):
env = 'Dev'
+
# Update step labels to reflect environment
self.deploy_widgets['Ensure target cluster']['step']['name'] = f"Ensure target cluster ({env})"
- self.deploy_widgets['Ensure target cluster']['label'].master.master.children['!label'].configure(text=f"Ensure target cluster ({env})")
+ # Use the correct way to update the label text in the UI
+ self.deploy_widgets['Ensure target cluster']['label'].master.winfo_children()[1].configure(text=f"Ensure target cluster ({env})")
- # Step 0: Build Prole macOS app
+ # Step 0: Generate prole.properties
+ self.generate_prole_properties(env)
+
+ # Step 1: Build Prole macOS app
self.update_deploy_step_status('Build Prole macOS app', 'running')
self.build_prole_app()
self.update_deploy_step_status('Build Prole macOS app', 'completed')
@@ -3352,13 +3417,22 @@ esac
version_file = PROJECT_ROOT / 'conf' / 'postgresql' / '.version'
if version_file.exists():
return version_file.read_text().strip()
- return '17.5-027' # Fallback
+ return '17.7-031' # Fallback
def build_docker_image(self):
"""Build prole-db Docker image"""
# Base local image tag (before pushing to registry)
version = self.get_prole_db_version()
image_tag = f'prole-db:{version}'
+
+ # Prepare build context: copy conf/postgresql to prole-db/postgresql
+ conf_src = PROJECT_ROOT / 'conf' / 'postgresql'
+ conf_dst = PROJECT_ROOT / 'prole-db' / 'postgresql'
+
+ if conf_dst.exists():
+ shutil.rmtree(conf_dst)
+ shutil.copytree(conf_src, conf_dst)
+
build_cmd = ['docker', 'build', '-t', image_tag]
# Add platform flag for Apple Silicon (ARM64 needs amd64 for compatibility)
@@ -3387,7 +3461,7 @@ esac
content = manifest_path.read_text()
import re
- # Update imageName: prole-db:17.5-027
+ # Update imageName: prole-db:17.7-031
new_content = re.sub(r'imageName: prole-db:.*', f'imageName: prole-db:{version}', content)
if new_content != content:
manifest_path.write_text(new_content)
diff --git a/installer/screen.py b/installer/screen.py
index 2084d88..eca79c2 100644
--- a/installer/screen.py
+++ b/installer/screen.py
@@ -29,7 +29,7 @@ def render_paragraph(app, text: str, y: int, wrap: int = 860):
def canvas_text(app, x: int, y: int, text: str, *, fill: str = '#1d1d1f',
font: tuple = ('Helvetica', 12), anchor: str = 'nw', width: int | None = None,
- justify: str | None = None) -> int:
+ justify: str | None = None, state: str | None = None) -> int:
"""Create a text item on the app's main canvas and track it.
Returns the created canvas item id.
@@ -41,34 +41,46 @@ def canvas_text(app, x: int, y: int, text: str, *, fill: str = '#1d1d1f',
kwargs['width'] = width
if justify is not None:
kwargs['justify'] = justify
+ if state is not None:
+ kwargs['state'] = state
item = app.bg_canvas.create_text(x, y, **kwargs)
app._canvas_items.append(item)
return item
def canvas_oval(app, x1: int, y1: int, x2: int, y2: int, *, fill: str | None = None,
- outline: str | None = None, width: int = 1) -> int:
+ outline: str | None = None, width: int = 1, state: str | None = None) -> int:
"""Create an oval on the app's main canvas and track it."""
if getattr(app, 'bg_canvas', None) is None:
return -1
- item = app.bg_canvas.create_oval(x1, y1, x2, y2, fill=fill or '', outline=outline or '', width=width)
+ kwargs = dict(fill=fill or '', outline=outline or '', width=width)
+ if state is not None:
+ kwargs['state'] = state
+ item = app.bg_canvas.create_oval(x1, y1, x2, y2, **kwargs)
app._canvas_items.append(item)
return item
def canvas_rectangle(app, x1: int, y1: int, x2: int, y2: int, *, outline: str = '#6e6e73',
- width: int = 1, fill: str | None = None) -> int:
+ width: int = 1, fill: str | None = None, state: str | None = None) -> int:
if getattr(app, 'bg_canvas', None) is None:
return -1
- item = app.bg_canvas.create_rectangle(x1, y1, x2, y2, outline=outline, width=width, fill=fill or '')
+ kwargs = dict(outline=outline, width=width, fill=fill or '')
+ if state is not None:
+ kwargs['state'] = state
+ item = app.bg_canvas.create_rectangle(x1, y1, x2, y2, **kwargs)
app._canvas_items.append(item)
return item
-def canvas_line(app, x1: int, y1: int, x2: int, y2: int, *, fill: str = '#1d1d1f', width: int = 2) -> int:
+def canvas_line(app, x1: int, y1: int, x2: int, y2: int, *, fill: str = '#1d1d1f',
+ width: int = 2, state: str | None = None) -> int:
if getattr(app, 'bg_canvas', None) is None:
return -1
- item = app.bg_canvas.create_line(x1, y1, x2, y2, fill=fill, width=width)
+ kwargs = dict(fill=fill, width=width)
+ if state is not None:
+ kwargs['state'] = state
+ item = app.bg_canvas.create_line(x1, y1, x2, y2, **kwargs)
app._canvas_items.append(item)
return item
diff --git a/k8s/prole/prole-db.yaml b/k8s/prole/prole-db.yaml
index 63df221..31d0dc5 100644
--- a/k8s/prole/prole-db.yaml
+++ b/k8s/prole/prole-db.yaml
@@ -4,7 +4,7 @@ metadata:
name: prole-db
spec:
instances: 3
- imageName: prole-db:17.5-027
+ imageName: prole-db:17.7-031
postgresUID: 100
postgresGID: 101
maxSyncReplicas: 1
@@ -14,7 +14,14 @@ spec:
shared_buffers: 256MB
pg_stat_statements.max: '10000'
pg_stat_statements.track: all
+ shared_preload_libraries:
+ - pg_stat_statements
+ - pg_tde
pg_hba:
+ # allow password access from remote hosts if environment is "#dev"
+ - local all postgres trust
+ - host all postgres all scram-sha-256
+ - host prole prole-db all scram-sha-256
- host all all all scram-sha-256
- hostssl prole prole-db all scram-sha-256
@@ -28,6 +35,7 @@ spec:
name: prole-db-user
postInitTemplateSQL:
- CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
+ postInitSQL:
- CREATE EXTENSION IF NOT EXISTS pg_tde;
enableSuperuserAccess: true
diff --git a/k8s/prole/prole-deployment.yaml b/k8s/prole/prole-deployment.yaml
index 01471f3..48b3470 100644
--- a/k8s/prole/prole-deployment.yaml
+++ b/k8s/prole/prole-deployment.yaml
@@ -44,4 +44,5 @@ spec:
secret:
secretName: prole-nginx-tls
- name: web-root
- emptyDir: {}
+ configMap:
+ name: prole-index-html
diff --git a/prole-db/Dockerfile b/prole-db/Dockerfile
index 8eb1446..a8a9c84 100644
--- a/prole-db/Dockerfile
+++ b/prole-db/Dockerfile
@@ -79,7 +79,7 @@ COPY prole-db-entrypoint.sh /usr/local/bin/prole-entrypoint.sh
COPY 10_pg_tde_openbao.sh /docker-entrypoint-initdb.d/10_pg_tde_openbao.sh
# staged copies of the pgdata/*.conf files
-COPY conf/postgresql/*.conf /etc/postgresql/17/main/
+COPY postgresql/*.conf /etc/postgresql/17/main/
RUN chown postgres:postgres /etc/postgresql/17/main/*.conf
RUN set -eux; \
diff --git a/prole-tools-app/Sources/Config.swift b/prole-tools-app/Sources/Config.swift
index 3d74d26..1b5a62b 100644
--- a/prole-tools-app/Sources/Config.swift
+++ b/prole-tools-app/Sources/Config.swift
@@ -89,7 +89,7 @@ final class Config {
}
}
- // Services list (name + hostname + port). Defaults: k3d + svc.prole.org
+ // Services list (name + hostname + port).
var services: [ServiceEndpoint] {
get {
let items: [ServiceEndpoint] = loadIndexed(prefix: "svc") { idx in
@@ -100,8 +100,11 @@ final class Config {
}
if !items.isEmpty { return items }
return [
- ServiceEndpoint(name: "k3d", host: "localhost", port: 6443),
- ServiceEndpoint(name: "svc.prole.org", host: "svc.prole.org", port: 443)
+ ServiceEndpoint(name: "K3D", host: "localhost", port: 6443),
+ ServiceEndpoint(name: "Prometheus", host: "localhost", port: 9090),
+ ServiceEndpoint(name: "Grafana", host: "localhost", port: 3000),
+ ServiceEndpoint(name: "OpenBAO", host: "localhost", port: 8200),
+ ServiceEndpoint(name: "PostgreSQL", host: "localhost", port: 5432)
]
}
set {
@@ -147,8 +150,8 @@ final class Config {
// Convenience computed values used by Status/Checker
var primaryService: ServiceEndpoint? {
- // Prefer "svc.prole.org" entry, else first
- return services.first(where: { $0.name == "svc.prole.org" }) ?? services.first
+ // Prefer "K3D" entry, else first
+ return services.first(where: { $0.name == "K3D" }) ?? services.first
}
var localService: ServiceEndpoint? {
return services.first(where: { $0.name.lowercased() == "k3d" })
diff --git a/prole-tools-app/Sources/OverlayWindow.swift b/prole-tools-app/Sources/OverlayWindow.swift
index 4160be7..b44c5a5 100644
--- a/prole-tools-app/Sources/OverlayWindow.swift
+++ b/prole-tools-app/Sources/OverlayWindow.swift
@@ -92,7 +92,7 @@ final class OverlayWindowController: NSWindowController {
func showContextMenu() {
guard let window = window, window.isVisible else { return }
let menu = NSMenu(title: "Prole Tools")
- menu.addItem(withTitle: statusView.aggregateStatusSummary(), action: nil, keyEquivalent: "")
+ menu.addItem(withTitle: "Prole Tools — System Status", action: nil, keyEquivalent: "")
menu.addItem(.separator())
menu.addItem(withTitle: "Refresh Now", action: #selector(refreshNow), keyEquivalent: "r").target = self
menu.addItem(withTitle: "Restart Port Forwards", action: #selector(resetPortForwards), keyEquivalent: "").target = self
diff --git a/prole-tools-app/Sources/PFScriptBridge.swift b/prole-tools-app/Sources/PFScriptBridge.swift
index c8523e0..ed7639c 100644
--- a/prole-tools-app/Sources/PFScriptBridge.swift
+++ b/prole-tools-app/Sources/PFScriptBridge.swift
@@ -16,7 +16,11 @@ enum PFScriptBridge {
static func status() -> (code: Int32, out: String, err: String) { runScript(arg: "status") }
static func dbStatus() -> (code: Int32, out: String, err: String) {
- runRawCommand(command: "kubecolor cnpg status prole-db", description: "kubecolor cnpg status prole-db")
+ runRawCommand(command: "kubectl cnpg status prole-db", description: "kubectl cnpg status prole-db")
+ }
+
+ static func cnpgStatus() -> (code: Int32, out: String, err: String) {
+ runRawCommand(command: "kubectl cnpg status prole-db | head -10", description: "kubectl cnpg status prole-db | head -10")
}
private static func runRawCommand(command: String, description: String) -> (code: Int32, out: String, err: String) {
diff --git a/prole-tools-app/Sources/ServiceChecker.swift b/prole-tools-app/Sources/ServiceChecker.swift
index 16de4a1..2a6f0e9 100644
--- a/prole-tools-app/Sources/ServiceChecker.swift
+++ b/prole-tools-app/Sources/ServiceChecker.swift
@@ -13,22 +13,18 @@ final class ServiceChecker {
private let refreshInterval: TimeInterval = 30
// Public reachability flags
- private(set) var svcReachable = false
- private(set) var raspberryReachable = false
- private(set) var piReachable = false
- private(set) var localReachable = false
-
- // Latency measurements (ms)
- private(set) var svcLatency: Int = -1
- private(set) var raspberryLatency: Int = -1
- private(set) var piLatency: Int = -1
- private(set) var localLatency: Int = -1
+ private(set) var k3dReachable = false
+ private(set) var prometheusReachable = false
+ private(set) var grafanaReachable = false
+ private(set) var openbaoReachable = false
+ private(set) var postgresReachable = false
// Last error messages (for tooltips when red)
- private(set) var svcError: String? = nil
- private(set) var raspberryError: String? = nil
- private(set) var piError: String? = nil
- private(set) var localError: String? = nil
+ private(set) var k3dError: String? = nil
+ private(set) var prometheusError: String? = nil
+ private(set) var grafanaError: String? = nil
+ private(set) var openbaoError: String? = nil
+ private(set) var postgresError: String? = nil
// Generic per-endpoint state so UI can query dynamically from Preferences
struct EndpointState: Equatable {
@@ -89,7 +85,7 @@ final class ServiceChecker {
let group = DispatchGroup()
// clear previous errors before a new round (legacy fields)
- svcError = nil; raspberryError = nil; piError = nil; localError = nil
+ k3dError = nil; prometheusError = nil; grafanaError = nil; openbaoError = nil; postgresError = nil
let cfg = Config.shared
// Iterate Services from Preferences
@@ -100,16 +96,26 @@ final class ServiceChecker {
let key = svc.name
let st = EndpointState(reachable: ok, latencyMs: ms, error: err, lastChecked: Date())
self.serviceStates[key] = st
- // Update legacy convenience fields for UI compatibility
- if let primary = cfg.primaryService, primary.name == svc.name {
- self.svcReachable = ok
- self.svcLatency = ms
- self.svcError = err
- }
- if let local = cfg.localService, local.name == svc.name {
- self.localReachable = ok
- self.localLatency = ms
- self.localError = err
+
+ // Update specific flags
+ switch svc.name {
+ case "K3D":
+ self.k3dReachable = ok
+ self.k3dError = err
+ case "Prometheus":
+ self.prometheusReachable = ok
+ self.prometheusError = err
+ case "Grafana":
+ self.grafanaReachable = ok
+ self.grafanaError = err
+ case "OpenBAO":
+ self.openbaoReachable = ok
+ self.openbaoError = err
+ case "PostgreSQL":
+ self.postgresReachable = ok
+ self.postgresError = err
+ default:
+ break
}
group.leave()
}
@@ -117,10 +123,6 @@ final class ServiceChecker {
// Iterate Kubernetes endpoints from Preferences
let kubes = cfg.kubernetes
- if kubes.isEmpty {
- raspberryReachable = false; raspberryLatency = -1; raspberryError = "no kubernetes endpoints configured"
- piReachable = false; piLatency = -1; piError = nil
- }
for (idx, k) in kubes.enumerated() {
group.enter()
tcpPing(host: k.host, port: UInt16(k.port)) { [weak self] ok, ms, err in
@@ -128,16 +130,6 @@ final class ServiceChecker {
let key = "\(k.host):\(k.port)"
let st = EndpointState(reachable: ok, latencyMs: ms, error: err, lastChecked: Date())
self.kubeStates[key] = st
- // Maintain two-LED aggregate legacy fields for first two entries
- if idx == 0 {
- self.raspberryReachable = ok
- self.raspberryLatency = ms
- self.raspberryError = err
- } else if idx == 1 {
- self.piReachable = ok
- self.piLatency = ms
- self.piError = err
- }
group.leave()
}
}
@@ -209,34 +201,17 @@ final class ServiceChecker {
}
// Tooltips
- func tooltipForSvc() -> String {
- guard let svc = Config.shared.primaryService else { return "No primary service configured" }
- if svcReachable { return "\(svc.host):\(svc.port) — reachable (\(svcLatency) ms)" }
- var s = "\(svc.host):\(svc.port) — unreachable"
- if let e = svcError { s += "\nError: \(e)" }
- return s
- }
- func tooltipForAggregateK3s() -> String {
- let kubes = Config.shared.kubernetes
- let rHost = kubes.first?.host ?? "-"
- let pHost = kubes.count > 1 ? kubes[1].host : "-"
- var r = raspberryReachable ? "\(rHost) ✓ (\(raspberryLatency) ms)" : "\(rHost) ✗"
- var p = piReachable ? "\(pHost) ✓ (\(piLatency) ms)" : "\(pHost) ✗"
- if !raspberryReachable, let e = raspberryError { r += " — \(e)" }
- if !piReachable, let e = piError { p += " — \(e)" }
- let overall: String
- switch (raspberryReachable, piReachable) {
- case (true, true): overall = "overall: green"
- case (true, false), (false, true): overall = "overall: yellow"
- default: overall = "overall: red"
- }
- return "k3s aggregate — \(overall)\n\(r)\n\(p)"
- }
- func tooltipForLocal() -> String {
- guard let local = Config.shared.localService else { return "No local service configured" }
- if localReachable { return "k3d \(local.host):\(local.port) — reachable (\(localLatency) ms)" }
- var s = "k3d \(local.host):\(local.port) — unreachable"
- if let e = localError { s += "\nError: \(e)" }
+ func tooltipFor(name: String) -> String {
+ let st = serviceStates[name]
+ let reachable = st?.reachable ?? false
+ let error = st?.error
+ let latency = st?.latencyMs ?? -1
+ let host = Config.shared.services.first(where: { $0.name == name })?.host ?? "unknown"
+ let port = Config.shared.services.first(where: { $0.name == name })?.port ?? 0
+
+ if reachable { return "\(name) (\(host):\(port)) — reachable (\(latency) ms)" }
+ var s = "\(name) (\(host):\(port)) — unreachable"
+ if let e = error { s += "\nError: \(e)" }
return s
}
diff --git a/prole-tools-app/Sources/StatusView.swift b/prole-tools-app/Sources/StatusView.swift
index c177d6b..fd063fd 100644
--- a/prole-tools-app/Sources/StatusView.swift
+++ b/prole-tools-app/Sources/StatusView.swift
@@ -4,15 +4,8 @@ final class StatusView: NSView {
private let light1 = TrafficLight()
private let light2 = TrafficLight()
private let light3 = TrafficLight()
- private let timestampLabel: NSTextField = {
- let tf = NSTextField(labelWithString: "0000-00-00 00:00:00 +00:00")
- tf.textColor = .secondaryLabelColor
- tf.alignment = .left
- tf.font = NSFont.monospacedDigitSystemFont(ofSize: 10, weight: .regular)
- tf.setContentHuggingPriority(.required, for: .horizontal)
- tf.setContentCompressionResistancePriority(.required, for: .horizontal)
- return tf
- }()
+ private let light4 = TrafficLight()
+ private let light5 = TrafficLight()
private let marquee = MarqueeView()
private let maximizeButton: NSButton = {
let b = NSButton(title: "□", target: nil, action: nil)
@@ -49,7 +42,7 @@ final class StatusView: NSView {
wantsLayer = true
translatesAutoresizingMaskIntoConstraints = false
- let stack = NSStackView(views: [light1, light2, light3, timestampLabel, marquee, maximizeButton])
+ let stack = NSStackView(views: [light1, light2, light3, light4, light5, marquee, maximizeButton])
stack.orientation = .horizontal
stack.alignment = .centerY
stack.distribution = .equalSpacing
@@ -102,56 +95,56 @@ final class StatusView: NSView {
@objc private func updateLights() {
guard let c = checker else { return }
- // Light 1: svc.prole.org:443
- light1.state = c.svcReachable ? .green : .red
- light1.toolTip = c.tooltipForSvc()
+ let services = Config.shared.services
+
+ // Light 1: K3D
+ if let s = services.first(where: { $0.name == "K3D" }) {
+ let state = c.serviceStates[s.name]
+ light1.state = (state?.reachable ?? false) ? .green : .red
+ light1.toolTip = c.tooltipFor(name: s.name)
+ }
- // Light 2: aggregate raspberry + pi
- let count = (c.raspberryReachable ? 1 : 0) + (c.piReachable ? 1 : 0)
- light2.state = count == 2 ? .green : (count == 1 ? .yellow : .red)
- light2.toolTip = c.tooltipForAggregateK3s()
+ // Light 2: Prometheus
+ if let s = services.first(where: { $0.name == "Prometheus" }) {
+ let state = c.serviceStates[s.name]
+ light2.state = (state?.reachable ?? false) ? .green : .red
+ light2.toolTip = c.tooltipFor(name: s.name)
+ }
- // Light 3: localhost k3d
- light3.state = c.localReachable ? .green : .red
- light3.toolTip = c.tooltipForLocal()
-
- // Timestamp label
- let now = Date()
- timestampLabel.stringValue = timeFormatter.string(from: now)
- timestampLabel.toolTip = "Last updated: " + fullFormatter.string(from: now)
+ // Light 3: Grafana
+ if let s = services.first(where: { $0.name == "Grafana" }) {
+ let state = c.serviceStates[s.name]
+ light3.state = (state?.reachable ?? false) ? .green : .red
+ light3.toolTip = c.tooltipFor(name: s.name)
+ }
+
+ // Light 4: OpenBAO
+ if let s = services.first(where: { $0.name == "OpenBAO" }) {
+ let state = c.serviceStates[s.name]
+ light4.state = (state?.reachable ?? false) ? .green : .red
+ light4.toolTip = c.tooltipFor(name: s.name)
+ }
+
+ // Light 5: PostgreSQL
+ if let s = services.first(where: { $0.name == "PostgreSQL" }) {
+ let state = c.serviceStates[s.name]
+ light5.state = (state?.reachable ?? false) ? .green : .red
+ light5.toolTip = c.tooltipFor(name: s.name)
+ }
// Update marquee text with status summary and ensure it scrolls
- let msg = aggregateStatusSummary()
- marquee.setText(msg)
- marquee.toolTip = msg
- needsDisplay = true
+ // For the scrolling status, use new contents: scroll the output of kubectl cnpg status | head -10
+ DispatchQueue.global(qos: .utility).async {
+ let res = PFScriptBridge.cnpgStatus()
+ let msg = res.out.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "Waiting for PostgreSQL status..." : res.out.replacingOccurrences(of: "\n", with: " • ")
+ DispatchQueue.main.async { [weak self] in
+ self?.marquee.setText(msg)
+ self?.marquee.toolTip = msg
+ self?.needsDisplay = true
+ }
+ }
}
- func aggregateStatusSummary() -> String {
- guard let c = checker else { return "No status" }
- let cfg = Config.shared
- let svcText: String = {
- if let svc = cfg.primaryService {
- return c.svcReachable ? "\(svc.host):\(svc.port) ✓" : "\(svc.host):\(svc.port) ✗"
- }
- return "svc (unset) ✗"
- }()
- let aggText: String = {
- let kube = cfg.kubernetes.first
- let name = kube?.host ?? "k3s"
- return "k3s: \(name) \(c.raspberryReachable ? "✓" : "✗")"
- }()
- let locText: String = {
- if let local = cfg.localService {
- return c.localReachable ? "k3d \(local.host):\(local.port) ✓" : "k3d \(local.host):\(local.port) ✗"
- }
- return "k3d (unset) ✗"
- }()
- let svc = svcText
- let agg = aggText
- let loc = locText
- return [svc, agg, loc].joined(separator: " • ")
- }
// MARK: - Button actions
@objc private func didTapMaximize() {
diff --git a/src/get.sh b/src/get.sh
index f1b6511..7b3c612 100755
--- a/src/get.sh
+++ b/src/get.sh
@@ -4,9 +4,9 @@ set -ex
brew update && brew install wget
-if [ ! -e postgresql-17.5.tar.gz ]; then
- wget https://ftp.postgresql.org/pub/source/v17.5/postgresql-17.5.tar.gz
+if [ ! -e postgresql-17.7.tar.gz ]; then
+ wget https://ftp.postgresql.org/pub/source/v17.7/postgresql-17.7.tar.gz
fi
-if [ ! -e postgresql-17.5.tar.gz.md5 ]; then
- wget https://ftp.postgresql.org/pub/source/v17.5/postgresql-17.5.tar.gz.md5
+if [ ! -e postgresql-17.7.tar.gz.md5 ]; then
+ wget https://ftp.postgresql.org/pub/source/v17.7/postgresql-17.7.tar.gz.md5
fi