mirror of
https://github.com/dredx/prole.git
synced 2026-09-27 04:34:30 +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
|
# build prole-db Docker image
|
||||||
```commandline
|
```commandline
|
||||||
cd prole-db
|
cd prole-db
|
||||||
docker build -t prole-db:17.5-027 .
|
docker build -t prole-db:17.7-031 .
|
||||||
k3d image import prole-db:17.5-027 -c prole-service-cluster
|
k3d image import prole-db:17.7-031 -c prole-service-cluster
|
||||||
docker tag prole-db:17.5-027 k8s.prole.org:5000/prole-db:17.5-027
|
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.5-027
|
docker push k8s.prole.org:5000/prole-db:17.7-031
|
||||||
# vim ~/.docker/daemon.json
|
# vim ~/.docker/daemon.json
|
||||||
# "insecure-registries": [ "k3.localhost:5000", "k8s.prole.org:5000" ]
|
# "insecure-registries": [ "k3.localhost:5000", "k8s.prole.org:5000" ]
|
||||||
```
|
```
|
||||||
|
|||||||
@ -12,4 +12,4 @@ docker run --rm -it -u root \
|
|||||||
--network=bridge \
|
--network=bridge \
|
||||||
--entrypoint bash \
|
--entrypoint bash \
|
||||||
--restart=no \
|
--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"
|
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
|
local token url
|
||||||
if [[ -f "$OPENBAO_TOKEN_FILE" ]]; then
|
if [[ -f "$OPENBAO_TOKEN_FILE" ]]; then
|
||||||
token=$(cat "$OPENBAO_TOKEN_FILE")
|
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" "$priv_b64" | base64 -d >"$ADMIN_PRIV"
|
||||||
printf "%s" "$pub_b64" | base64 -d >"$ADMIN_PUB"
|
printf "%s" "$pub_b64" | base64 -d >"$ADMIN_PUB"
|
||||||
chmod 0600 "$ADMIN_PRIV"
|
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
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -f "$ADMIN_PRIV" && -f "$ADMIN_PUB" ]]; then
|
if [[ -f "$ADMIN_PRIV" && -f "$ADMIN_PUB" ]]; then
|
||||||
echo "Using local admin key pair at $SECRETS_DIR"
|
echo "Using local admin key pair at $SECRETS_DIR"
|
||||||
return 0
|
return 0
|
||||||
@ -177,6 +196,7 @@ patch_cnpg_cluster_for_auth() {
|
|||||||
\"parameters\": {\"krb_srvname\": null},
|
\"parameters\": {\"krb_srvname\": null},
|
||||||
\"pg_hba\": [
|
\"pg_hba\": [
|
||||||
\"local all postgres trust\",
|
\"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 gss include_realm=1 krb_realm=$REALM\",
|
||||||
\"host all all all scram-sha-256\"
|
\"host all all all scram-sha-256\"
|
||||||
]
|
]
|
||||||
@ -196,7 +216,7 @@ initialize() {
|
|||||||
echo "Ensuring port-forward for OpenBao is active ..."
|
echo "Ensuring port-forward for OpenBao is active ..."
|
||||||
"$SCRIPT_DIR/init_port_forwards.sh" restart openbao
|
"$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
|
apply_cnpg_admin_secret
|
||||||
ensure_krb5_conf_configmap
|
ensure_krb5_conf_configmap
|
||||||
# generate_tls_if_missing
|
# generate_tls_if_missing
|
||||||
@ -225,8 +245,27 @@ 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
|
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' ..."
|
echo "Creating CloudNative-PG cluster and resources for '$CNPG_CLUSTER_NAME' ..."
|
||||||
|
|
||||||
kubectl apply -k "$SCRIPT_DIR/../k8s/prole"
|
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 ..."
|
echo "Initializing and patching cluster ..."
|
||||||
initialize
|
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)
|
p2.place(x=x_field, y=y-12, width=300)
|
||||||
self._overlay_widgets.append(p2)
|
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):
|
def _render_init_scripts_page(self):
|
||||||
self._render_title('Initialization Scripts', y=40)
|
self._render_title('Initialization Scripts', y=40)
|
||||||
self._render_paragraph('Running initialization scripts to set up OpenBao, CloudNative-PG, and Port Forwards.', y=80)
|
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"""
|
"""Start the deployment process"""
|
||||||
threading.Thread(target=self.run_deployment, daemon=True).start()
|
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):
|
def run_deployment(self):
|
||||||
"""Run the deployment steps"""
|
"""Run the deployment steps"""
|
||||||
try:
|
try:
|
||||||
@ -3062,11 +3122,16 @@ exec "$DIR/ProleTools.bin" "$@"
|
|||||||
env = self.deploy_environment.get().strip()
|
env = self.deploy_environment.get().strip()
|
||||||
if env not in ('Dev', 'Service', 'Prod'):
|
if env not in ('Dev', 'Service', 'Prod'):
|
||||||
env = 'Dev'
|
env = 'Dev'
|
||||||
|
|
||||||
# Update step labels to reflect environment
|
# 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']['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.update_deploy_step_status('Build Prole macOS app', 'running')
|
||||||
self.build_prole_app()
|
self.build_prole_app()
|
||||||
self.update_deploy_step_status('Build Prole macOS app', 'completed')
|
self.update_deploy_step_status('Build Prole macOS app', 'completed')
|
||||||
@ -3352,13 +3417,22 @@ esac
|
|||||||
version_file = PROJECT_ROOT / 'conf' / 'postgresql' / '.version'
|
version_file = PROJECT_ROOT / 'conf' / 'postgresql' / '.version'
|
||||||
if version_file.exists():
|
if version_file.exists():
|
||||||
return version_file.read_text().strip()
|
return version_file.read_text().strip()
|
||||||
return '17.5-027' # Fallback
|
return '17.7-031' # Fallback
|
||||||
|
|
||||||
def build_docker_image(self):
|
def build_docker_image(self):
|
||||||
"""Build prole-db Docker image"""
|
"""Build prole-db Docker image"""
|
||||||
# Base local image tag (before pushing to registry)
|
# Base local image tag (before pushing to registry)
|
||||||
version = self.get_prole_db_version()
|
version = self.get_prole_db_version()
|
||||||
image_tag = f'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]
|
build_cmd = ['docker', 'build', '-t', image_tag]
|
||||||
|
|
||||||
# Add platform flag for Apple Silicon (ARM64 needs amd64 for compatibility)
|
# Add platform flag for Apple Silicon (ARM64 needs amd64 for compatibility)
|
||||||
@ -3387,7 +3461,7 @@ esac
|
|||||||
|
|
||||||
content = manifest_path.read_text()
|
content = manifest_path.read_text()
|
||||||
import re
|
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)
|
new_content = re.sub(r'imageName: prole-db:.*', f'imageName: prole-db:{version}', content)
|
||||||
if new_content != content:
|
if new_content != content:
|
||||||
manifest_path.write_text(new_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',
|
def canvas_text(app, x: int, y: int, text: str, *, fill: str = '#1d1d1f',
|
||||||
font: tuple = ('Helvetica', 12), anchor: str = 'nw', width: int | None = None,
|
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.
|
"""Create a text item on the app's main canvas and track it.
|
||||||
|
|
||||||
Returns the created canvas item id.
|
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
|
kwargs['width'] = width
|
||||||
if justify is not None:
|
if justify is not None:
|
||||||
kwargs['justify'] = justify
|
kwargs['justify'] = justify
|
||||||
|
if state is not None:
|
||||||
|
kwargs['state'] = state
|
||||||
item = app.bg_canvas.create_text(x, y, **kwargs)
|
item = app.bg_canvas.create_text(x, y, **kwargs)
|
||||||
app._canvas_items.append(item)
|
app._canvas_items.append(item)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
def canvas_oval(app, x1: int, y1: int, x2: int, y2: int, *, fill: str | None = None,
|
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."""
|
"""Create an oval on the app's main canvas and track it."""
|
||||||
if getattr(app, 'bg_canvas', None) is None:
|
if getattr(app, 'bg_canvas', None) is None:
|
||||||
return -1
|
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)
|
app._canvas_items.append(item)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
def canvas_rectangle(app, x1: int, y1: int, x2: int, y2: int, *, outline: str = '#6e6e73',
|
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:
|
if getattr(app, 'bg_canvas', None) is None:
|
||||||
return -1
|
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)
|
app._canvas_items.append(item)
|
||||||
return 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:
|
if getattr(app, 'bg_canvas', None) is None:
|
||||||
return -1
|
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)
|
app._canvas_items.append(item)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ metadata:
|
|||||||
name: prole-db
|
name: prole-db
|
||||||
spec:
|
spec:
|
||||||
instances: 3
|
instances: 3
|
||||||
imageName: prole-db:17.5-027
|
imageName: prole-db:17.7-031
|
||||||
postgresUID: 100
|
postgresUID: 100
|
||||||
postgresGID: 101
|
postgresGID: 101
|
||||||
maxSyncReplicas: 1
|
maxSyncReplicas: 1
|
||||||
@ -14,7 +14,14 @@ spec:
|
|||||||
shared_buffers: 256MB
|
shared_buffers: 256MB
|
||||||
pg_stat_statements.max: '10000'
|
pg_stat_statements.max: '10000'
|
||||||
pg_stat_statements.track: all
|
pg_stat_statements.track: all
|
||||||
|
shared_preload_libraries:
|
||||||
|
- pg_stat_statements
|
||||||
|
- pg_tde
|
||||||
pg_hba:
|
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
|
- host all all all scram-sha-256
|
||||||
- hostssl prole prole-db all scram-sha-256
|
- hostssl prole prole-db all scram-sha-256
|
||||||
|
|
||||||
@ -28,6 +35,7 @@ spec:
|
|||||||
name: prole-db-user
|
name: prole-db-user
|
||||||
postInitTemplateSQL:
|
postInitTemplateSQL:
|
||||||
- CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
|
- CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
|
||||||
|
postInitSQL:
|
||||||
- CREATE EXTENSION IF NOT EXISTS pg_tde;
|
- CREATE EXTENSION IF NOT EXISTS pg_tde;
|
||||||
|
|
||||||
enableSuperuserAccess: true
|
enableSuperuserAccess: true
|
||||||
|
|||||||
@ -44,4 +44,5 @@ spec:
|
|||||||
secret:
|
secret:
|
||||||
secretName: prole-nginx-tls
|
secretName: prole-nginx-tls
|
||||||
- name: web-root
|
- 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
|
COPY 10_pg_tde_openbao.sh /docker-entrypoint-initdb.d/10_pg_tde_openbao.sh
|
||||||
|
|
||||||
# staged copies of the pgdata/*.conf files
|
# 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 chown postgres:postgres /etc/postgresql/17/main/*.conf
|
||||||
|
|
||||||
RUN set -eux; \
|
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] {
|
var services: [ServiceEndpoint] {
|
||||||
get {
|
get {
|
||||||
let items: [ServiceEndpoint] = loadIndexed(prefix: "svc") { idx in
|
let items: [ServiceEndpoint] = loadIndexed(prefix: "svc") { idx in
|
||||||
@ -100,8 +100,11 @@ final class Config {
|
|||||||
}
|
}
|
||||||
if !items.isEmpty { return items }
|
if !items.isEmpty { return items }
|
||||||
return [
|
return [
|
||||||
ServiceEndpoint(name: "k3d", host: "localhost", port: 6443),
|
ServiceEndpoint(name: "K3D", host: "localhost", port: 6443),
|
||||||
ServiceEndpoint(name: "svc.prole.org", host: "svc.prole.org", port: 443)
|
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 {
|
set {
|
||||||
@ -147,8 +150,8 @@ final class Config {
|
|||||||
|
|
||||||
// Convenience computed values used by Status/Checker
|
// Convenience computed values used by Status/Checker
|
||||||
var primaryService: ServiceEndpoint? {
|
var primaryService: ServiceEndpoint? {
|
||||||
// Prefer "svc.prole.org" entry, else first
|
// Prefer "K3D" entry, else first
|
||||||
return services.first(where: { $0.name == "svc.prole.org" }) ?? services.first
|
return services.first(where: { $0.name == "K3D" }) ?? services.first
|
||||||
}
|
}
|
||||||
var localService: ServiceEndpoint? {
|
var localService: ServiceEndpoint? {
|
||||||
return services.first(where: { $0.name.lowercased() == "k3d" })
|
return services.first(where: { $0.name.lowercased() == "k3d" })
|
||||||
|
|||||||
@ -92,7 +92,7 @@ final class OverlayWindowController: NSWindowController {
|
|||||||
func showContextMenu() {
|
func showContextMenu() {
|
||||||
guard let window = window, window.isVisible else { return }
|
guard let window = window, window.isVisible else { return }
|
||||||
let menu = NSMenu(title: "Prole Tools")
|
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(.separator())
|
||||||
menu.addItem(withTitle: "Refresh Now", action: #selector(refreshNow), keyEquivalent: "r").target = self
|
menu.addItem(withTitle: "Refresh Now", action: #selector(refreshNow), keyEquivalent: "r").target = self
|
||||||
menu.addItem(withTitle: "Restart Port Forwards", action: #selector(resetPortForwards), keyEquivalent: "").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 status() -> (code: Int32, out: String, err: String) { runScript(arg: "status") }
|
||||||
|
|
||||||
static func dbStatus() -> (code: Int32, out: String, err: String) {
|
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) {
|
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
|
private let refreshInterval: TimeInterval = 30
|
||||||
|
|
||||||
// Public reachability flags
|
// Public reachability flags
|
||||||
private(set) var svcReachable = false
|
private(set) var k3dReachable = false
|
||||||
private(set) var raspberryReachable = false
|
private(set) var prometheusReachable = false
|
||||||
private(set) var piReachable = false
|
private(set) var grafanaReachable = false
|
||||||
private(set) var localReachable = false
|
private(set) var openbaoReachable = false
|
||||||
|
private(set) var postgresReachable = 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
|
|
||||||
|
|
||||||
// Last error messages (for tooltips when red)
|
// Last error messages (for tooltips when red)
|
||||||
private(set) var svcError: String? = nil
|
private(set) var k3dError: String? = nil
|
||||||
private(set) var raspberryError: String? = nil
|
private(set) var prometheusError: String? = nil
|
||||||
private(set) var piError: String? = nil
|
private(set) var grafanaError: String? = nil
|
||||||
private(set) var localError: 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
|
// Generic per-endpoint state so UI can query dynamically from Preferences
|
||||||
struct EndpointState: Equatable {
|
struct EndpointState: Equatable {
|
||||||
@ -89,7 +85,7 @@ final class ServiceChecker {
|
|||||||
let group = DispatchGroup()
|
let group = DispatchGroup()
|
||||||
|
|
||||||
// clear previous errors before a new round (legacy fields)
|
// 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
|
let cfg = Config.shared
|
||||||
// Iterate Services from Preferences
|
// Iterate Services from Preferences
|
||||||
@ -100,16 +96,26 @@ final class ServiceChecker {
|
|||||||
let key = svc.name
|
let key = svc.name
|
||||||
let st = EndpointState(reachable: ok, latencyMs: ms, error: err, lastChecked: Date())
|
let st = EndpointState(reachable: ok, latencyMs: ms, error: err, lastChecked: Date())
|
||||||
self.serviceStates[key] = st
|
self.serviceStates[key] = st
|
||||||
// Update legacy convenience fields for UI compatibility
|
|
||||||
if let primary = cfg.primaryService, primary.name == svc.name {
|
// Update specific flags
|
||||||
self.svcReachable = ok
|
switch svc.name {
|
||||||
self.svcLatency = ms
|
case "K3D":
|
||||||
self.svcError = err
|
self.k3dReachable = ok
|
||||||
}
|
self.k3dError = err
|
||||||
if let local = cfg.localService, local.name == svc.name {
|
case "Prometheus":
|
||||||
self.localReachable = ok
|
self.prometheusReachable = ok
|
||||||
self.localLatency = ms
|
self.prometheusError = err
|
||||||
self.localError = 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()
|
group.leave()
|
||||||
}
|
}
|
||||||
@ -117,10 +123,6 @@ final class ServiceChecker {
|
|||||||
|
|
||||||
// Iterate Kubernetes endpoints from Preferences
|
// Iterate Kubernetes endpoints from Preferences
|
||||||
let kubes = cfg.kubernetes
|
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() {
|
for (idx, k) in kubes.enumerated() {
|
||||||
group.enter()
|
group.enter()
|
||||||
tcpPing(host: k.host, port: UInt16(k.port)) { [weak self] ok, ms, err in
|
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 key = "\(k.host):\(k.port)"
|
||||||
let st = EndpointState(reachable: ok, latencyMs: ms, error: err, lastChecked: Date())
|
let st = EndpointState(reachable: ok, latencyMs: ms, error: err, lastChecked: Date())
|
||||||
self.kubeStates[key] = st
|
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()
|
group.leave()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -209,34 +201,17 @@ final class ServiceChecker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tooltips
|
// Tooltips
|
||||||
func tooltipForSvc() -> String {
|
func tooltipFor(name: String) -> String {
|
||||||
guard let svc = Config.shared.primaryService else { return "No primary service configured" }
|
let st = serviceStates[name]
|
||||||
if svcReachable { return "\(svc.host):\(svc.port) — reachable (\(svcLatency) ms)" }
|
let reachable = st?.reachable ?? false
|
||||||
var s = "\(svc.host):\(svc.port) — unreachable"
|
let error = st?.error
|
||||||
if let e = svcError { s += "\nError: \(e)" }
|
let latency = st?.latencyMs ?? -1
|
||||||
return s
|
let host = Config.shared.services.first(where: { $0.name == name })?.host ?? "unknown"
|
||||||
}
|
let port = Config.shared.services.first(where: { $0.name == name })?.port ?? 0
|
||||||
func tooltipForAggregateK3s() -> String {
|
|
||||||
let kubes = Config.shared.kubernetes
|
if reachable { return "\(name) (\(host):\(port)) — reachable (\(latency) ms)" }
|
||||||
let rHost = kubes.first?.host ?? "-"
|
var s = "\(name) (\(host):\(port)) — unreachable"
|
||||||
let pHost = kubes.count > 1 ? kubes[1].host : "-"
|
if let e = error { s += "\nError: \(e)" }
|
||||||
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)" }
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,15 +4,8 @@ final class StatusView: NSView {
|
|||||||
private let light1 = TrafficLight()
|
private let light1 = TrafficLight()
|
||||||
private let light2 = TrafficLight()
|
private let light2 = TrafficLight()
|
||||||
private let light3 = TrafficLight()
|
private let light3 = TrafficLight()
|
||||||
private let timestampLabel: NSTextField = {
|
private let light4 = TrafficLight()
|
||||||
let tf = NSTextField(labelWithString: "0000-00-00 00:00:00 +00:00")
|
private let light5 = TrafficLight()
|
||||||
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 marquee = MarqueeView()
|
private let marquee = MarqueeView()
|
||||||
private let maximizeButton: NSButton = {
|
private let maximizeButton: NSButton = {
|
||||||
let b = NSButton(title: "□", target: nil, action: nil)
|
let b = NSButton(title: "□", target: nil, action: nil)
|
||||||
@ -49,7 +42,7 @@ final class StatusView: NSView {
|
|||||||
wantsLayer = true
|
wantsLayer = true
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
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.orientation = .horizontal
|
||||||
stack.alignment = .centerY
|
stack.alignment = .centerY
|
||||||
stack.distribution = .equalSpacing
|
stack.distribution = .equalSpacing
|
||||||
@ -102,56 +95,56 @@ final class StatusView: NSView {
|
|||||||
|
|
||||||
@objc private func updateLights() {
|
@objc private func updateLights() {
|
||||||
guard let c = checker else { return }
|
guard let c = checker else { return }
|
||||||
// Light 1: svc.prole.org:443
|
let services = Config.shared.services
|
||||||
light1.state = c.svcReachable ? .green : .red
|
|
||||||
light1.toolTip = c.tooltipForSvc()
|
|
||||||
|
|
||||||
// Light 2: aggregate raspberry + pi
|
// Light 1: K3D
|
||||||
let count = (c.raspberryReachable ? 1 : 0) + (c.piReachable ? 1 : 0)
|
if let s = services.first(where: { $0.name == "K3D" }) {
|
||||||
light2.state = count == 2 ? .green : (count == 1 ? .yellow : .red)
|
let state = c.serviceStates[s.name]
|
||||||
light2.toolTip = c.tooltipForAggregateK3s()
|
light1.state = (state?.reachable ?? false) ? .green : .red
|
||||||
|
light1.toolTip = c.tooltipFor(name: s.name)
|
||||||
|
}
|
||||||
|
|
||||||
// Light 3: localhost k3d
|
// Light 2: Prometheus
|
||||||
light3.state = c.localReachable ? .green : .red
|
if let s = services.first(where: { $0.name == "Prometheus" }) {
|
||||||
light3.toolTip = c.tooltipForLocal()
|
let state = c.serviceStates[s.name]
|
||||||
|
light2.state = (state?.reachable ?? false) ? .green : .red
|
||||||
|
light2.toolTip = c.tooltipFor(name: s.name)
|
||||||
|
}
|
||||||
|
|
||||||
// Timestamp label
|
// Light 3: Grafana
|
||||||
let now = Date()
|
if let s = services.first(where: { $0.name == "Grafana" }) {
|
||||||
timestampLabel.stringValue = timeFormatter.string(from: now)
|
let state = c.serviceStates[s.name]
|
||||||
timestampLabel.toolTip = "Last updated: " + fullFormatter.string(from: now)
|
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
|
// Update marquee text with status summary and ensure it scrolls
|
||||||
let msg = aggregateStatusSummary()
|
// For the scrolling status, use new contents: scroll the output of kubectl cnpg status | head -10
|
||||||
marquee.setText(msg)
|
DispatchQueue.global(qos: .utility).async {
|
||||||
marquee.toolTip = msg
|
let res = PFScriptBridge.cnpgStatus()
|
||||||
needsDisplay = true
|
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
|
// MARK: - Button actions
|
||||||
@objc private func didTapMaximize() {
|
@objc private func didTapMaximize() {
|
||||||
|
|||||||
@ -4,9 +4,9 @@ set -ex
|
|||||||
|
|
||||||
brew update && brew install wget
|
brew update && brew install wget
|
||||||
|
|
||||||
if [ ! -e postgresql-17.5.tar.gz ]; then
|
if [ ! -e postgresql-17.7.tar.gz ]; then
|
||||||
wget https://ftp.postgresql.org/pub/source/v17.5/postgresql-17.5.tar.gz
|
wget https://ftp.postgresql.org/pub/source/v17.7/postgresql-17.7.tar.gz
|
||||||
fi
|
fi
|
||||||
if [ ! -e postgresql-17.5.tar.gz.md5 ]; then
|
if [ ! -e postgresql-17.7.tar.gz.md5 ]; then
|
||||||
wget https://ftp.postgresql.org/pub/source/v17.5/postgresql-17.5.tar.gz.md5
|
wget https://ftp.postgresql.org/pub/source/v17.7/postgresql-17.7.tar.gz.md5
|
||||||
fi
|
fi
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user