mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 16:24:32 +00:00
feat: implement encrypted development database with OpenBao and CNPG - Update install.py with password matching visual feedback and OpenBao integration - Enhance etc/init_openbao.sh to store database password in OpenBao KV - Update etc/init_cloudnative_pg.sh to synchronize database password from OpenBao to K8s secrets - Configure CNPG cluster in k8s/prole/prole-db.yaml with pg_hba for password authentication - Update various scripts and app sources for better service integration
This commit is contained in:
parent
528e362d95
commit
973920db07
@ -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" ]
|
||||
```
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 "<html><body><h1>Prole</h1></body></html>" > /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
|
||||
|
||||
453
etc/init_openbao.sh
Executable file
453
etc/init_openbao.sh
Executable file
@ -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" <<EOF
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: $OPENBAO_NAME
|
||||
namespace: $NAMESPACE
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: $OPENBAO_NAME
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: $OPENBAO_NAME
|
||||
spec:
|
||||
containers:
|
||||
- name: $OPENBAO_NAME
|
||||
image: $OPENBAO_IMAGE
|
||||
args: ["server","-dev","-dev-listen-address=0.0.0.0:8200"]
|
||||
ports:
|
||||
- containerPort: 8200
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: $OPENBAO_NAME
|
||||
namespace: $NAMESPACE
|
||||
spec:
|
||||
selector:
|
||||
app: $OPENBAO_NAME
|
||||
ports:
|
||||
- name: http
|
||||
port: 8200
|
||||
targetPort: 8200
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
generate_kerberos_configmap() {
|
||||
mkdir -p "$OPENBAO_MANIFEST_DIR"
|
||||
local domain_lower
|
||||
domain_lower=$(echo "$DOMAIN" | tr 'A-Z' 'a-z')
|
||||
|
||||
# Split KRB5_KDC on comma into bash array
|
||||
local IFS=','
|
||||
read -r -a __kdcs <<< "$KRB5_KDC"
|
||||
local kdc_lines=""
|
||||
local kdc
|
||||
for kdc in "${__kdcs[@]}"; do
|
||||
kdc_lines+=$' kdc = '"$kdc"$'\n'
|
||||
done
|
||||
local admin_server admin_line
|
||||
admin_server=${KRB5_ADMIN:-"${__kdcs[0]:-}"}
|
||||
admin_line=""
|
||||
if [[ -n "$admin_server" ]]; then
|
||||
# 8 spaces to remain within the YAML literal block indentation
|
||||
admin_line=$' admin_server = '"$admin_server"$'\n'
|
||||
fi
|
||||
|
||||
cat >"$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml" <<EOF
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: prole-krb5-conf
|
||||
namespace: $NAMESPACE
|
||||
data:
|
||||
krb5.conf: |
|
||||
[libdefaults]
|
||||
default_realm = $KRB5_REALM
|
||||
dns_lookup_realm = true
|
||||
dns_lookup_kdc = true
|
||||
|
||||
[realms]
|
||||
$KRB5_REALM = {
|
||||
${kdc_lines}${admin_line} }
|
||||
|
||||
[domain_realm]
|
||||
.$domain_lower = $KRB5_REALM
|
||||
$domain_lower = $KRB5_REALM
|
||||
EOF
|
||||
}
|
||||
|
||||
apply_k8s() {
|
||||
echo "Applying OpenBao manifest to namespace '$NAMESPACE' ..."
|
||||
kubectl apply -f "$OPENBAO_MANIFEST_DIR/deployment.yaml"
|
||||
echo "Applying Kerberos ConfigMap (external realm) to namespace '$NAMESPACE' ..."
|
||||
kubectl apply -f "$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml"
|
||||
}
|
||||
|
||||
wait_for_openbao() {
|
||||
echo "Waiting for OpenBao to become ready ..."
|
||||
kubectl rollout status deploy/$OPENBAO_NAME -n "$NAMESPACE" --timeout=120s
|
||||
}
|
||||
|
||||
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"
|
||||
echo "Configuring KV at $svc ..."
|
||||
|
||||
# Check if kv/ is already mounted
|
||||
if curl -sS -H "X-Vault-Token: $token" "$svc/v1/sys/mounts" | grep -q '"kv/":'; then
|
||||
echo "KV path 'kv/' is already enabled."
|
||||
else
|
||||
curl -sS -H "X-Vault-Token: $token" -X POST "$svc/v1/sys/mounts/kv" \
|
||||
-d '{"type":"kv","options":{"version":"2"}}' >/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
|
||||
82
install.py
82
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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -44,4 +44,5 @@ spec:
|
||||
secret:
|
||||
secretName: prole-nginx-tls
|
||||
- name: web-root
|
||||
emptyDir: {}
|
||||
configMap:
|
||||
name: prole-index-html
|
||||
|
||||
@ -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; \
|
||||
|
||||
@ -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" })
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user