prole/etc/init_cloudnative_pg.sh
chrisfu 906392d462 feat(installer): improve UI and add test coverage for core features
- Refactored installer UI with updated canvas rendering, sidebar navigation, and footer buttons.
- Enhanced styling for macOS compatibility and consistent design across controls.
- Added Pytest-based unit tests for `screen.py` and `config.py`.
- Expanded dependency catalog with new tools like `tshark` and `pyshark`.
- Improved error tolerance for background rendering and added placeholders for Kerberos configuration.
2026-01-08 21:51:30 -08:00

327 lines
11 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
set -euo pipefail
# init_cloudnative_pg.sh
# Purpose:
# - Distribute administrator ed25519 key pair to CloudNativePG as a Kubernetes Secret for cert auth
# - Configure Kerberos (GSSAPI) using external Kerberos KDC
# - Patch CNPG cluster to enable TLS and GSSAPI where possible
#
# Usage:
# ./init_cloudnative_pg.sh start|stop|status|restart
# ./init_cloudnative_pg.sh initialize # create/update k8s secrets/configs and patch CNPG
# ./init_cloudnative_pg.sh update|reload # re-apply/patch
#
# Requirements:
# - init_openbao.sh has been run (OpenBao running in k8s)
# - $PROLE_HOME/env.sh or $HOME/.prole/env.sh defining PROLE_SERVICE
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Load env without leaking our positional args to the env script (some env.sh may `exec "$@"`).
__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"
else
echo "ERROR: Missing env. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2
exit 1
fi
set -- "${__PROLE_SAVED_ARGS[@]}"
unset __PROLE_SAVED_ARGS
if [[ -z "${PROLE_SERVICE:-}" ]]; then
echo "ERROR: PROLE_SERVICE is not defined in env." >&2
exit 1
fi
ACTION=${1:-}
CNPG_CLUSTER_NAME=${2:-${CNPG_CLUSTER_NAME:-prole-db}}
NAMESPACE=${NAMESPACE:-prole}
OPENBAO_NAME=${OPENBAO_NAME:-openbao}
REALM=${REALM:-PROLE.ORG}
DOMAIN=${DOMAIN:-prole.org}
KRB5_KDC=${KRB5_KDC:-}
KRB5_ADMIN=${KRB5_ADMIN:-}
CNPG_MANIFEST="$SCRIPT_DIR/../k8s/prole/prole-db.yaml"
SECRETS_DIR="$PROLE_SERVICE/secrets"
ADMIN_PRIV="$SECRETS_DIR/admin_ed25519.key"
ADMIN_PUB="$SECRETS_DIR/admin_ed25519.pub"
OPENBAO_TOKEN_FILE="$SECRETS_DIR/openbao-root-token"
ensure_tools() {
for t in kubectl curl openssl base64 jq; do
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
done
}
# Resolve OpenBao URL: prefer explicit env, then localhost port-forward, then cluster DNS
bao_service_url() {
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
echo "$PROLE_OPENBAO_URL"
return 0
fi
# Prefer standard local port-forward managed by etc/init_port_forwards.sh
if curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:18200"
return 0
fi
echo "http://$OPENBAO_NAME.$NAMESPACE.svc.cluster.local:8200"
}
fetch_admin_keys_and_db_pass_from_bao_or_local() {
local token url
if [[ -f "$OPENBAO_TOKEN_FILE" ]]; then
token=$(cat "$OPENBAO_TOKEN_FILE")
else
token=""
fi
url=$(bao_service_url)
if [[ -n "$token" ]]; then
echo "Attempting to read admin key pair from OpenBao kv/prole/admin ..."
if curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/admin" | jq -e '.data.data' >/dev/null 2>&1; then
local priv_b64 pub_b64
priv_b64=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/admin" | jq -r '.data.data.admin_private_key_b64')
pub_b64=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/admin" | jq -r '.data.data.admin_public_key_b64')
printf "%s" "$priv_b64" | base64 -d >"$ADMIN_PRIV"
printf "%s" "$pub_b64" | base64 -d >"$ADMIN_PUB"
chmod 0600 "$ADMIN_PRIV"
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
fi
echo "ERROR: Could not obtain admin key pair from OpenBao and no local files found." >&2
exit 1
}
apply_cnpg_admin_secret() {
echo "Creating/updating Secret cnpg-admin-key ..."
kubectl create secret generic cnpg-admin-key -n "$NAMESPACE" \
--from-file=admin.key="$ADMIN_PRIV" \
--from-file=admin.pub="$ADMIN_PUB" \
--dry-run=client -o yaml | kubectl apply -f -
}
ensure_krb5_conf_configmap() {
echo "Creating/updating ConfigMap prole-krb5-conf ..."
local kdc_val admin_val
if [[ -n "$KRB5_KDC" ]]; then
kdc_val="$KRB5_KDC"
admin_val=${KRB5_ADMIN:-$(echo "$KRB5_KDC" | cut -d, -f1)}
else
kdc_val="kdc.$DOMAIN"
admin_val="kdc.$DOMAIN"
fi
local TMP
TMP=$(mktemp)
cat >"$TMP" <<EOF
[libdefaults]
default_realm = $REALM
dns_lookup_realm = true
dns_lookup_kdc = true
[realms]
$REALM = {
kdc = $kdc_val
admin_server = $admin_val
}
[domain_realm]
.$DOMAIN = $REALM
$DOMAIN = $REALM
EOF
kubectl -n "$NAMESPACE" create configmap prole-krb5-conf --from-file=krb5.conf="$TMP" --dry-run=client -o yaml | kubectl apply -f -
rm -f "$TMP"
}
generate_tls_if_missing() {
local ca_secret_name="${CNPG_CLUSTER_NAME}-ca"
if kubectl -n "$NAMESPACE" get secret "$ca_secret_name" >/dev/null 2>&1; then
echo "CA secret $ca_secret_name already exists; skipping generation."
return 0
fi
echo "Generating self-signed CA (RSA 4096) for CNPG ..."
local TMPD
TMPD=$(mktemp -d)
openssl genrsa -out "$TMPD/ca.key" 4096
openssl req -x509 -new -key "$TMPD/ca.key" -out "$TMPD/ca.crt" -days 3650 -subj "/CN=Prole CNPG CA"
kubectl -n "$NAMESPACE" create secret generic "$ca_secret_name" \
--from-file=ca.crt="$TMPD/ca.crt" \
--from-file=ca.key="$TMPD/ca.key" \
--dry-run=client -o yaml | kubectl apply -f -
rm -rf "$TMPD"
}
patch_cnpg_cluster_for_auth() {
echo "Patching CNPG Cluster $CNPG_CLUSTER_NAME to enable GSSAPI and fix config (best-effort) ..."
# Best-effort patch; schema may vary with CNPG version.
# We remove krb_srvname as it is unrecognized in some PG 17 builds.
# We revert certificates to default to avoid operator TLS issues.
kubectl -n "$NAMESPACE" patch cluster "$CNPG_CLUSTER_NAME" --type merge -p "{
\"spec\": {
\"postgresql\": {
\"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\"
]
},
\"certificates\": {
\"serverCASecret\": null,
\"clientCASecret\": null
}
}
}" >/dev/null || echo "Note: patch may need adjustment for your CNPG version."
}
initialize() {
ensure_tools
# Ensure port-forward is running for OpenBao (dependency)
echo "Ensuring port-forward for OpenBao is active ..."
"$SCRIPT_DIR/init_port_forwards.sh" restart openbao
fetch_admin_keys_and_db_pass_from_bao_or_local
apply_cnpg_admin_secret
ensure_krb5_conf_configmap
# generate_tls_if_missing
patch_cnpg_cluster_for_auth
# Ensure port-forward is running for Postgres (local access)
echo "Ensuring port-forward for Postgres is active ..."
"$SCRIPT_DIR/init_port_forwards.sh" restart postgres
echo "Initialization complete for CNPG + Kerberos + cert artifacts."
}
update_reload() {
initialize
}
case "$ACTION" in
recreate)
ensure_tools
"$0" delete "$CNPG_CLUSTER_NAME"
"$0" create "$CNPG_CLUSTER_NAME"
;;
create)
ensure_tools
echo "Installing CloudNative-PG operator ..."
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
;;
delete)
ensure_tools
echo "Deleting all resources for '$CNPG_CLUSTER_NAME' ..."
kubectl delete -k "$SCRIPT_DIR/../k8s/prole" --ignore-not-found
;;
start)
ensure_tools
if [[ ! -f "$CNPG_MANIFEST" ]]; then
echo "ERROR: CNPG manifest not found at $CNPG_MANIFEST" >&2
exit 1
fi
echo "Starting CloudNative-PG cluster from $CNPG_MANIFEST in namespace $NAMESPACE..."
kubectl apply -n "$NAMESPACE" -f "$CNPG_MANIFEST"
;;
stop)
ensure_tools
if [[ ! -f "$CNPG_MANIFEST" ]]; then
echo "ERROR: CNPG manifest not found at $CNPG_MANIFEST" >&2
exit 1
fi
echo "Stopping CloudNative-PG cluster using $CNPG_MANIFEST ..."
kubectl delete -f "$CNPG_MANIFEST" --ignore-not-found
;;
status)
ensure_tools
echo "--- CloudNative-PG Cluster Status ($CNPG_CLUSTER_NAME) ---"
if kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" >/dev/null 2>&1; then
kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME"
echo ""
echo "CNPG Plugin Status:"
if kubectl cnpg version >/dev/null 2>&1; then
kubectl cnpg status "$CNPG_CLUSTER_NAME" -n "$NAMESPACE"
else
echo "Note: 'kubectl cnpg' plugin not found; skipping detailed status."
fi
else
echo "Cluster '$CNPG_CLUSTER_NAME' not found in namespace '$NAMESPACE'."
fi
;;
restart)
ensure_tools
"$0" stop
"$0" start
;;
initialize)
initialize
;;
update|reload)
update_reload
;;
*)
echo "Usage: $0 {create|delete|recreate|start|stop|status|restart|initialize|update|reload} [dbname]" >&2
exit 2
;;
esac