Stabilize deliverable k3d pipeline and refactor installer components

- Finalized stable, repeatable reset logic for the k3d pipeline.

- Refactored installer into modular components: core, milestone, runner, and state.

- Introduced new UI abstractions with support for ncurses and Tkinter.

- Updated initialization scripts and configurations for CloudNativePG, Kerberos, OpenBao, and Monitoring.

- Improved pipeline repair and port-forwarding mechanisms.
This commit is contained in:
chrisfu 2026-02-15 00:03:24 -08:00
parent d2efb835b0
commit 6b89ea23d7
29 changed files with 14133 additions and 13414 deletions

View File

@ -106,7 +106,6 @@ PORT_FORWARD_K3S_MAPPING_1 = id=grafana;namespace=${NAMESPACE};target=svc/grafan
PORT_FORWARD_K3S_MAPPING_2 = id=opentofu;namespace=${NAMESPACE};target=svc/opentofu;address=0.0.0.0;hostPort=8080;servicePort=8080;protocol=TCP;description=OpenTofu UI/API (0.0.0.0:8080)
PORT_FORWARD_K3S_MAPPING_3 = id=openbao;namespace=${NAMESPACE};target=svc/openbao;address=0.0.0.0;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenVault/OpenBao UI/API (0.0.0.0:8200)
PORT_FORWARD_K3S_MAPPING_4 = id=postgres;namespace=${NAMESPACE};target=svc/prole-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL external (0.0.0.0:5432)
PORT_FORWARD_K3S_MAPPING_5 = id=registry;namespace=${NAMESPACE};target=svc/registry;address=0.0.0.0;hostPort=5000;servicePort=5000;protocol=TCP;description=Docker registry (0.0.0.0:5000)
[System Environment]
PROLE_CONF = ${PROLE_CONF}

View File

@ -105,7 +105,6 @@ PORT_FORWARD_K3S_MAPPING_1 = id=grafana;namespace=${NAMESPACE};target=svc/grafan
PORT_FORWARD_K3S_MAPPING_2 = id=opentofu;namespace=${NAMESPACE};target=svc/opentofu;address=0.0.0.0;hostPort=8080;servicePort=8080;protocol=TCP;description=OpenTofu UI/API (0.0.0.0:8080)
PORT_FORWARD_K3S_MAPPING_3 = id=openbao;namespace=${NAMESPACE};target=svc/openbao;address=0.0.0.0;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenVault/OpenBao UI/API (0.0.0.0:8200)
PORT_FORWARD_K3S_MAPPING_4 = id=postgres;namespace=${NAMESPACE};target=svc/prole-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL external (0.0.0.0:5432)
PORT_FORWARD_K3S_MAPPING_5 = id=registry;namespace=${NAMESPACE};target=svc/registry;address=0.0.0.0;hostPort=5000;servicePort=5000;protocol=TCP;description=Docker registry (0.0.0.0:5000)
[System Environment]
PROLE_CONF = ${PROLE_CONF}

View File

@ -1,12 +1,12 @@
; Prole Master Configuration File
; Generated by install.py on 2026-02-14 11:50:44
; Generated by install.py on 2026-02-14 23:59:31
; This file is used as input for Ansible deployment and k8s cluster creation.
[User]
; User-editable values; derived values below reference these by default.
NAMESPACE = prole-db-101
PROLE_CONF = ${PROLE_HOME}/conf
PROLE_DATA = /opt/prole/data/chrisfu
PROLE_DATA = /Users/chrisfu/.prole/data
PROLE_HOME = /Users/chrisfu/dev/prole
PROLE_LOGS = /opt/prole/logs/chrisfu
PROLE_SERVICE = ${PROLE_HOME}/etc
@ -96,14 +96,14 @@ KDC_AUTO_DETECTED = 10.0.0.3
KERBEROS_AUTO_ENABLED = True
[Port Forwards]
PORT_FORWARD_K3D_MAPPING_1 = id=registry;namespace=default;target=svc/registry;address=0.0.0.0;hostPort=5000;servicePort=5000;protocol=TCP;description=Docker registry
PORT_FORWARD_K3D_MAPPING_2 = id=garage;namespace=default;target=svc/garage;address=0.0.0.0;hostPort=3900;servicePort=3900;protocol=TCP;description=Garage S3
PORT_FORWARD_K3D_MAPPING_3 = id=openbao;namespace=default;target=svc/openbao;address=127.0.0.1;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenBao
PORT_FORWARD_K3D_MAPPING_4 = id=opentofu;namespace=default;target=svc/opentofu;address=0.0.0.0;hostPort=8080;servicePort=8080;protocol=TCP;description=OpenTofu
PORT_FORWARD_K3D_MAPPING_5 = id=dashboard;namespace=kubernetes-dashboard;target=svc/kubernetes-dashboard-kong-proxy;address=127.0.0.1;hostPort=8443;servicePort=443;protocol=TCP;description=Kubernetes Dashboard
PORT_FORWARD_K3D_MAPPING_6 = id=postgres;namespace=${NAMESPACE};target=svc/prole-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL (primary)
PORT_FORWARD_K3D_MAPPING_7 = id=grafana;namespace=${NAMESPACE};target=svc/grafana;address=0.0.0.0;hostPort=3000;servicePort=3000;protocol=TCP;description=Grafana UI
PORT_FORWARD_K3D_MAPPING_8 = id=argocd;namespace=default;target=svc/argocd-server;address=0.0.0.0;hostPort=8081;servicePort=80;protocol=TCP;description=ArgoCD
PORT_FORWARD_K3D_MAPPING_1 = id=garage;namespace=default;target=svc/garage;address=0.0.0.0;hostPort=3900;servicePort=3900;protocol=TCP;description=Garage S3
PORT_FORWARD_K3D_MAPPING_2 = id=openbao;namespace=default;target=svc/openbao;address=127.0.0.1;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenBao
PORT_FORWARD_K3D_MAPPING_3 = id=opentofu;namespace=default;target=svc/opentofu;address=0.0.0.0;hostPort=8080;servicePort=8080;protocol=TCP;description=OpenTofu
PORT_FORWARD_K3D_MAPPING_4 = id=dashboard;namespace=kubernetes-dashboard;target=svc/kubernetes-dashboard-kong-proxy;address=127.0.0.1;hostPort=8443;servicePort=443;protocol=TCP;description=Kubernetes Dashboard
PORT_FORWARD_K3D_MAPPING_5 = id=postgres;namespace=${NAMESPACE};target=svc/prole-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL (primary)
PORT_FORWARD_K3D_MAPPING_6 = id=grafana;namespace=${NAMESPACE};target=svc/grafana;address=0.0.0.0;hostPort=3000;servicePort=3000;protocol=TCP;description=Grafana UI
PORT_FORWARD_K3D_MAPPING_7 = id=argocd;namespace=default;target=svc/argocd-server;address=0.0.0.0;hostPort=8081;servicePort=80;protocol=TCP;description=ArgoCD
PORT_FORWARD_K3D_MAPPING_8 = id=prometheus;namespace=default;target=svc/prometheus-community-kube-prometheus;address=127.0.0.1;hostPort=9090;servicePort=9090;protocol=TCP;description=Prometheus
[System Environment]
PROLE_CONF = ${PROLE_CONF}
@ -139,6 +139,9 @@ KERBEROS_ENABLED = true
SUPABASE_ENABLED = false
[Database Creation]
DB_NAME = prole-db-101
DB_PASSWORD_SET = true
DB_USER = root
[Initialize Cluster]
ENVIRONMENT = k3d-prole-dev-cluster
@ -172,11 +175,12 @@ LOCAL_REGISTRY_INTERNAL = k3d-prole-registry.localhost:5000
STATUS = Built
[Initialization Scripts]
; No configuration values captured yet for this section.
STATUS = Completed
[Deployment]
MODE = k3d
STATUS = Deployed
TARGET = prole-dev-cluster
[Install]
; No configuration values captured yet for this section.
STATUS = Finished

View File

@ -105,7 +105,6 @@ PORT_FORWARD_K3S_MAPPING_1 = id=grafana;namespace=${NAMESPACE};target=svc/grafan
PORT_FORWARD_K3S_MAPPING_2 = id=opentofu;namespace=${NAMESPACE};target=svc/opentofu;address=0.0.0.0;hostPort=8080;servicePort=8080;protocol=TCP;description=OpenTofu UI/API (0.0.0.0:8080)
PORT_FORWARD_K3S_MAPPING_3 = id=openbao;namespace=${NAMESPACE};target=svc/openbao;address=0.0.0.0;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenVault/OpenBao UI/API (0.0.0.0:8200)
PORT_FORWARD_K3S_MAPPING_4 = id=postgres;namespace=${NAMESPACE};target=svc/prole-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL external (0.0.0.0:5432)
PORT_FORWARD_K3S_MAPPING_5 = id=registry;namespace=${NAMESPACE};target=svc/registry;address=0.0.0.0;hostPort=5000;servicePort=5000;protocol=TCP;description=Docker registry (0.0.0.0:5000)
[System Environment]
PROLE_CONF = ${PROLE_CONF}

2
env.sh
View File

@ -5,7 +5,7 @@
# shellcheck shell=bash
export PROLE_HOME="/Users/chrisfu/dev/prole"
export PROLE_CONF="/Users/chrisfu/dev/prole/conf"
export PROLE_DATA="/opt/prole/data/chrisfu"
export PROLE_DATA="/Users/chrisfu/.prole/data"
export PROLE_LOGS="/opt/prole/logs/chrisfu"
export PROLE_SERVICE="/Users/chrisfu/dev/prole/etc"
export NAMESPACE="prole-db-101"

View File

@ -82,6 +82,7 @@ ADMIN_PRIV_GENERIC="$SECRETS_DIR/admin.key"
ADMIN_PUB_GENERIC="$SECRETS_DIR/admin.pub"
OPENBAO_TOKEN_FILE="$SECRETS_DIR/openbao-root-token"
BAO_NAMESPACE="${NAMESPACE:-default}"
OPENBAO_NAMESPACE="${OPENBAO_NAMESPACE:-${SERVICE_NAMESPACE:-default}}"
BAO_PATH_PREFIX="prole/${BAO_NAMESPACE}"
BAO_PATH_ADMIN="${BAO_PATH_PREFIX}/admin"
BAO_PATH_DB="${BAO_PATH_PREFIX}/db"
@ -163,10 +164,21 @@ sync_manifest_image() {
openbao_url() {
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
echo "$PROLE_OPENBAO_URL"
return 0
fi
if prole_is_in_cluster; then
echo "http://openbao.${OPENBAO_NAMESPACE}.svc.cluster.local:8200"
return 0
fi
if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:8200"
return 0
elif 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
else
echo "http://openbao.${NAMESPACE}.svc.cluster.local:8200"
echo ""
return 0
fi
}
@ -184,7 +196,7 @@ fetch_openbao_secret() {
local token url
token=$(openbao_token)
url=$(openbao_url)
if [[ -z "$token" ]]; then
if [[ -z "$token" || -z "$url" ]]; then
echo ""
return 0
fi
@ -218,7 +230,7 @@ ensure_grafana_admin_secret() {
local token url
token=$(openbao_token)
url=$(openbao_url)
if [[ -n "$token" ]]; then
if [[ -n "$token" && -n "$url" ]]; then
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$url/v1/kv/data/$BAO_PATH_MONITORING" \
-d "{\"data\":{\"grafana_admin_password\":\"$db_pw\"}}" >/dev/null || true
@ -248,7 +260,7 @@ ensure_cnpg_operator() {
get_latest_barman_plugin_version() {
local version tag
tag=$(curl -s "https://api.github.com/repos/cloudnative-pg/plugin-barman-cloud/releases/latest" | jq -r '.tag_name' || echo "")
tag=$(curl -s --connect-timeout 5 --max-time 10 "https://api.github.com/repos/cloudnative-pg/plugin-barman-cloud/releases/latest" | jq -r '.tag_name' || echo "")
if [[ -z "$tag" || "$tag" == "null" ]]; then
echo "v${BARMAN_PLUGIN_FALLBACK_VERSION}"
return 0
@ -278,7 +290,7 @@ cert_manager_ready() {
get_latest_cert_manager_version() {
local tag
tag=$(curl -s "https://api.github.com/repos/cert-manager/cert-manager/releases/latest" | jq -r '.tag_name' || echo "")
tag=$(curl -s --connect-timeout 5 --max-time 10 "https://api.github.com/repos/cert-manager/cert-manager/releases/latest" | jq -r '.tag_name' || echo "")
if [[ -z "$tag" || "$tag" == "null" ]]; then
echo "v${CERT_MANAGER_FALLBACK_VERSION}"
return 0
@ -355,7 +367,20 @@ ensure_barman_plugin() {
local plugin_url
plugin_url=$(resolve_barman_plugin_manifest_url)
echo "Installing Barman Cloud plugin from ${plugin_url} ..."
kubectl apply -f "$plugin_url"
local apply_out=""
if ! apply_out=$(kubectl apply -f "$plugin_url" 2>&1); then
echo "$apply_out" >&2
if echo "$apply_out" | grep -qi "webhook.cert-manager.io"; then
echo "WARN: cert-manager webhook error detected; restarting cert-manager components and retrying..." >&2
kubectl -n cert-manager rollout restart deploy/cert-manager deploy/cert-manager-webhook deploy/cert-manager-cainjector >/dev/null 2>&1 || true
kubectl -n cert-manager rollout status deploy/cert-manager-webhook --timeout=180s >/dev/null 2>&1 || true
kubectl -n cert-manager rollout status deploy/cert-manager --timeout=180s >/dev/null 2>&1 || true
kubectl -n cert-manager rollout status deploy/cert-manager-cainjector --timeout=180s >/dev/null 2>&1 || true
kubectl apply -f "$plugin_url" || true
fi
else
printf '%s\n' "$apply_out"
fi
if ! wait_for_barman_crd 120; then
echo "WARN: Barman Cloud ObjectStore CRD not ready after install." >&2
@ -549,7 +574,7 @@ apply_cnpg_cluster_manifest() {
}
wait_for_cnpg_pods() {
local timeout=${1:-300}
local timeout=${1:-${CNPG_WAIT_TIMEOUT:-900}}
local start_time
start_time=$(date +%s)
@ -885,18 +910,26 @@ reset_and_reinit() {
wait_for_cnpg_pods 300
}
# Resolve OpenBao URL: prefer explicit env, then localhost port-forward, then cluster DNS
# Resolve OpenBao URL: prefer explicit env, then in-cluster service, then localhost port-forward
bao_service_url() {
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
echo "$PROLE_OPENBAO_URL"
return 0
fi
if prole_is_in_cluster; then
echo "http://$OPENBAO_NAME.${OPENBAO_NAMESPACE:-${SERVICE_NAMESPACE:-default}}.svc.cluster.local:8200"
return 0
fi
if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:8200"
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"
echo ""
}
fetch_admin_keys_and_db_pass_from_bao_or_local() {
@ -907,7 +940,7 @@ fetch_admin_keys_and_db_pass_from_bao_or_local() {
token=""
fi
url=$(bao_service_url)
if [[ -n "$token" ]]; then
if [[ -n "$token" && -n "$url" ]]; then
echo "Attempting to read admin key pair from OpenBao kv/$BAO_PATH_ADMIN ..."
if curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/$BAO_PATH_ADMIN" | jq -e '.data.data' >/dev/null 2>&1; then
local priv_b64 pub_b64
@ -947,6 +980,24 @@ fetch_admin_keys_and_db_pass_from_bao_or_local() {
fi
fi
# Fallback: use DB_PASSWORD from env if OpenBao is unreachable or empty
if ! kubectl -n "$NAMESPACE" get secret prole-db-user >/dev/null 2>&1; then
local db_pass="${DB_PASSWORD:-}"
if [[ -n "$db_pass" && "$db_pass" != '${OPENBAO:'* && "$db_pass" != '${PROLE_SECRET:'* ]]; then
echo "Updating database user secret 'prole-db-user' from DB_PASSWORD env ..."
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 DB_PASSWORD env ..."
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
if [[ -f "$ADMIN_PRIV_GENERIC" && -f "$ADMIN_PUB_GENERIC" ]]; then
echo "Using local admin key pair at $SECRETS_DIR"
return 0

View File

@ -54,6 +54,18 @@ PROLE_KDC_TRUST_SHARED_PASSWORD=${PROLE_KDC_TRUST_SHARED_PASSWORD:-}
KRB5_KDC=${KRB5_KDC:-}
KRB5_ADMIN=${KRB5_ADMIN:-}
local_registry_enabled() {
case "${PROLE_ENABLE_LOCAL_REGISTRY:-${ENABLE_LOCAL_REGISTRY:-}}" in
1|true|TRUE|True|yes|YES|Yes|on|ON|On) return 0 ;;
esac
return 1
}
if ! local_registry_enabled; then
PROLE_KDC_REGISTRY_HOST=""
PROLE_KDC_REGISTRY_INTERNAL=""
fi
log() { printf '%s\n' "$*"; }
err() { printf '%s\n' "$*" >&2; }
@ -243,25 +255,29 @@ resolve_prole_kdc_defaults() {
mode="${PROLE_MODE:-}"
fi
local k3s_host=""
if [[ "$mode" == "k3s" ]]; then
k3s_host=$(registry_host_from_url "${PROLE_K3S_SERVER:-${K3S_SERVER_URL:-}}")
if [[ -n "$k3s_host" ]] && ([[ -z "${PROLE_KDC_REGISTRY_HOST:-}" ]] || [[ "$PROLE_KDC_REGISTRY_HOST" == "localhost:5000" ]] || [[ "$PROLE_KDC_REGISTRY_HOST" == *"k3d"* ]]); then
PROLE_KDC_REGISTRY_HOST="${k3s_host}:5000"
if local_registry_enabled; then
if [[ "$mode" == "k3s" ]]; then
k3s_host=$(registry_host_from_url "${PROLE_K3S_SERVER:-${K3S_SERVER_URL:-}}")
if [[ -n "$k3s_host" ]] && ([[ -z "${PROLE_KDC_REGISTRY_HOST:-}" ]] || [[ "$PROLE_KDC_REGISTRY_HOST" == "localhost:5000" ]] || [[ "$PROLE_KDC_REGISTRY_HOST" == *"k3d"* ]]); then
PROLE_KDC_REGISTRY_HOST="${k3s_host}:5000"
fi
fi
fi
if [[ -z "$PROLE_KDC_REGISTRY_INTERNAL" ]]; then
if [[ -n "${KDC_NAMESPACE:-}" ]]; then
PROLE_KDC_REGISTRY_INTERNAL="registry.${KDC_NAMESPACE}.svc.cluster.local:5000"
elif [[ -n "${SERVICE_NAMESPACE:-}" ]]; then
PROLE_KDC_REGISTRY_INTERNAL="registry.${SERVICE_NAMESPACE}.svc.cluster.local:5000"
elif [[ -n "${NAMESPACE:-}" ]]; then
PROLE_KDC_REGISTRY_INTERNAL="registry.${NAMESPACE}.svc.cluster.local:5000"
if [[ -z "$PROLE_KDC_REGISTRY_INTERNAL" ]]; then
if [[ -n "${KDC_NAMESPACE:-}" ]]; then
PROLE_KDC_REGISTRY_INTERNAL="registry.${KDC_NAMESPACE}.svc.cluster.local:5000"
elif [[ -n "${SERVICE_NAMESPACE:-}" ]]; then
PROLE_KDC_REGISTRY_INTERNAL="registry.${SERVICE_NAMESPACE}.svc.cluster.local:5000"
elif [[ -n "${NAMESPACE:-}" ]]; then
PROLE_KDC_REGISTRY_INTERNAL="registry.${NAMESPACE}.svc.cluster.local:5000"
fi
fi
fi
if [[ "$mode" == "k3s" ]]; then
if [[ -n "$k3s_host" ]] && ([[ -z "$PROLE_KDC_REGISTRY_INTERNAL" ]] || [[ "$PROLE_KDC_REGISTRY_INTERNAL" == *"k3d"* ]] || [[ "$PROLE_KDC_REGISTRY_INTERNAL" == *".svc.cluster.local"* ]]); then
PROLE_KDC_REGISTRY_INTERNAL="${k3s_host}:5000"
if [[ "$mode" == "k3s" ]]; then
if [[ -n "$k3s_host" ]] && ([[ -z "$PROLE_KDC_REGISTRY_INTERNAL" ]] || [[ "$PROLE_KDC_REGISTRY_INTERNAL" == *"k3d"* ]] || [[ "$PROLE_KDC_REGISTRY_INTERNAL" == *".svc.cluster.local"* ]]); then
PROLE_KDC_REGISTRY_INTERNAL="${k3s_host}:5000"
fi
fi
else
PROLE_KDC_REGISTRY_INTERNAL=""
fi
if [[ -z "$PROLE_KDC_IMAGE" ]]; then
if [[ -n "$PROLE_KDC_REGISTRY_INTERNAL" ]]; then
@ -337,6 +353,14 @@ build_prole_kdc_image() {
fi
fi
if [[ -z "$PROLE_KDC_REGISTRY_HOST" && "$(normalized_mode)" == "k3d" ]]; then
if command -v k3d >/dev/null 2>&1; then
local cluster_name="${K3D_CLUSTER_NAME:-prole-dev-cluster}"
log "Importing authority image into k3d cluster: ${cluster_name}"
k3d image import "$local_tag" -c "$cluster_name" >/dev/null 2>&1 || true
fi
fi
if [[ -n "$PROLE_KDC_REGISTRY_INTERNAL" ]]; then
PROLE_KDC_IMAGE="${PROLE_KDC_REGISTRY_INTERNAL}/${PROLE_KDC_IMAGE_NAME}:${PROLE_KDC_IMAGE_TAG}"
elif [[ -n "$PROLE_KDC_REGISTRY_HOST" ]]; then
@ -554,6 +578,7 @@ ${dns_policy_block}
containers:
- name: kdc
image: ${PROLE_KDC_IMAGE}
imagePullPolicy: IfNotPresent
command: ["/bin/bash", "/opt/prole-kdc/entrypoint.sh"]
env:
- name: PROLE_KDC_REALM

View File

@ -49,6 +49,7 @@ KRB5_AD_TCP_PORTS=${KRB5_AD_TCP_PORTS:-"88 389 445 464 636"}
KRB5_AD_UDP_PORTS=${KRB5_AD_UDP_PORTS:-"88 464"}
CNPG_WAIT_TIMEOUT=${CNPG_WAIT_TIMEOUT:-300}
SERVICE_NAMESPACE=${SERVICE_NAMESPACE:-${NAMESPACE:-default}}
OPENBAO_NAMESPACE=${OPENBAO_NAMESPACE:-${SERVICE_NAMESPACE:-default}}
KRB5_AD_NAMESPACE=${KRB5_AD_NAMESPACE:-${SERVICE_NAMESPACE}}
log() { printf '%s\n' "$*"; }
@ -60,9 +61,9 @@ ensure_tools() {
done
}
cnpg_supports_pod_template() {
cnpg_supports_projected_volume_template() {
kubectl get crd clusters.postgresql.cnpg.io -o json 2>/dev/null \
| jq -e '.spec.versions[] | select(.name=="v1") | .schema.openAPIV3Schema.properties.spec.properties.podTemplate' >/dev/null
| jq -e '.spec.versions[] | select(.name=="v1") | .schema.openAPIV3Schema.properties.spec.properties.projectedVolumeTemplate' >/dev/null
}
sha256_stdin() {
@ -104,10 +105,21 @@ is_ip_address() {
openbao_url() {
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
echo "$PROLE_OPENBAO_URL"
return 0
fi
if prole_is_in_cluster; then
echo "http://openbao.${OPENBAO_NAMESPACE}.svc.cluster.local:8200"
return 0
fi
if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:8200"
return 0
elif 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
else
echo "http://openbao.${NAMESPACE}.svc.cluster.local:8200"
echo ""
return 0
fi
}
@ -125,7 +137,7 @@ fetch_openbao_secret() {
local token url
token=$(openbao_token)
url=$(openbao_url)
if [[ -z "$token" ]]; then
if [[ -z "$token" || -z "$url" ]]; then
echo ""
return 0
fi
@ -366,14 +378,25 @@ reload_cnpg_cluster() {
}
apply_krb5_env_to_cnpg() {
local conf_path="${1:-/projected/krb5/krb5.conf}"
local conf_hash=""
local conf
conf=$(get_krb5_conf)
if [[ -n "$conf" ]]; then
conf_hash=$(printf '%s' "$conf" | sha256_stdin)
fi
if ! kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" >/dev/null 2>&1; then
return 1
fi
local current_env updated_env
current_env=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o json 2>/dev/null | jq -c '.spec.env // []')
updated_env=$(printf '%s' "$current_env" | jq -c --arg name "KRB5_CONFIG" --arg value "/etc/prole/krb5.conf:/etc/krb5.conf" '
map(select(.name != $name)) + [{"name": $name, "value": $value}]
updated_env=$(printf '%s' "$current_env" | jq -c \
--arg name "KRB5_CONFIG" --arg value "$conf_path" \
--arg hash_name "PROLE_KRB5_CONF_SHA" --arg hash_value "$conf_hash" '
map(select(.name != $name and .name != $hash_name)) +
[{"name": $name, "value": $value}] +
(if $hash_value == "" then [] else [{"name": $hash_name, "value": $hash_value}] end)
')
kubectl -n "$NAMESPACE" patch cluster "$CNPG_CLUSTER_NAME" --type merge -p "{\"spec\":{\"env\":$updated_env}}" >/dev/null 2>&1 || true
@ -384,52 +407,29 @@ apply_krb5_conf_mount_to_cnpg() {
err "ERROR: Cluster '$CNPG_CLUSTER_NAME' not found in namespace '$NAMESPACE'."
return 1
fi
if ! cnpg_supports_pod_template; then
err "CNPG CRD does not support spec.podTemplate; falling back to in-pod krb5.conf update."
if ! cnpg_supports_projected_volume_template; then
err "CNPG CRD does not support spec.projectedVolumeTemplate; falling back to in-pod krb5.conf update."
return 1
fi
local conf conf_hash
local conf
conf=$(get_krb5_conf)
if [[ -z "$conf" ]]; then
err "Missing krb5.conf data in ConfigMap prole-krb5-conf."
return 1
fi
conf_hash=$(printf '%s' "$conf" | sha256_stdin)
log "Patching CNPG Cluster $CNPG_CLUSTER_NAME to mount krb5.conf (rolling update) ..."
if ! cat <<EOF | kubectl apply --server-side --field-manager=prole-kerberos -f -
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: ${CNPG_CLUSTER_NAME}
namespace: ${NAMESPACE}
spec:
podTemplate:
metadata:
annotations:
prole.dev/krb5-conf-hash: "${conf_hash}"
spec:
volumes:
- name: krb5-conf
configMap:
name: prole-krb5-conf
items:
- key: krb5.conf
path: krb5.conf
containers:
- name: postgres
env:
- name: KRB5_CONFIG
value: /etc/prole/krb5.conf:/etc/krb5.conf
volumeMounts:
- name: krb5-conf
mountPath: /etc/prole/krb5.conf
subPath: krb5.conf
readOnly: true
EOF
then
err "Note: unable to apply krb5.conf mount via server-side apply."
log "Patching CNPG Cluster $CNPG_CLUSTER_NAME to mount krb5.conf via projected volume ..."
if ! kubectl -n "$NAMESPACE" patch cluster "$CNPG_CLUSTER_NAME" --type merge -p "{
\"spec\": {
\"projectedVolumeTemplate\": {
\"sources\": [
{\"configMap\": {\"name\": \"prole-krb5-conf\", \"items\": [{\"key\": \"krb5.conf\", \"path\": \"krb5/krb5.conf\"}]}}
]
}
}
}" >/dev/null 2>&1; then
err "Note: unable to apply krb5.conf mount via patch."
return 1
fi
}
@ -450,11 +450,11 @@ apply_krb5_conf_to_cnpg_pods() {
fi
for pod in $pods; do
log "Updating /etc/prole/krb5.conf in pod $pod ..."
if printf '%s' "$conf" | kubectl -n "$NAMESPACE" exec -i "$pod" -c postgres -- sh -c 'mkdir -p /etc/prole && tee /etc/prole/krb5.conf /etc/krb5.conf >/dev/null'; then
log "[OK] Updated /etc/prole/krb5.conf in $pod"
log "Updating /controller/krb5.conf in pod $pod ..."
if printf '%s' "$conf" | kubectl -n "$NAMESPACE" exec -i "$pod" -c postgres -- sh -c 'tee /controller/krb5.conf >/dev/null'; then
log "[OK] Updated /controller/krb5.conf in $pod"
else
err "[WARN] Unable to write /etc/prole/krb5.conf in $pod (permission?)."
err "[WARN] Unable to write /controller/krb5.conf in $pod."
err " Consider mounting ConfigMap prole-krb5-conf into the CNPG pods or updating the image."
fi
done
@ -634,11 +634,12 @@ initialize() {
sync_prole_kdc_trust
ensure_krb5_conf_configmap
if apply_krb5_conf_mount_to_cnpg; then
apply_krb5_env_to_cnpg "/projected/krb5/krb5.conf"
wait_for_cnpg_pods "$CNPG_WAIT_TIMEOUT"
else
wait_for_cnpg_pods "$CNPG_WAIT_TIMEOUT"
apply_krb5_conf_to_cnpg_pods
apply_krb5_env_to_cnpg
apply_krb5_env_to_cnpg "/controller/krb5.conf"
fi
patch_cnpg_cluster_for_auth || true
reload_cnpg_cluster || true

View File

@ -188,10 +188,21 @@ check_samba_connectivity() {
openbao_url() {
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
echo "$PROLE_OPENBAO_URL"
return 0
fi
if prole_is_in_cluster; then
echo "http://openbao.${SERVICE_NAMESPACE:-${NAMESPACE:-default}}.svc.cluster.local:8200"
return 0
fi
if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:8200"
return 0
elif 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
else
echo "http://openbao.${NAMESPACE}.svc.cluster.local:8200"
echo ""
return 0
fi
}
@ -209,7 +220,7 @@ fetch_openbao_secret() {
local token url
token=$(openbao_token)
url=$(openbao_url)
if [[ -z "$token" ]]; then
if [[ -z "$token" || -z "$url" ]]; then
echo ""
return 0
fi

View File

@ -123,10 +123,21 @@ helm_upgrade_with_retry() {
openbao_url() {
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
echo "$PROLE_OPENBAO_URL"
return 0
fi
if prole_is_in_cluster; then
echo "http://openbao.${SERVICE_NAMESPACE:-${NAMESPACE:-default}}.svc.cluster.local:8200"
return 0
fi
if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:8200"
return 0
elif 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
else
echo "http://openbao.${NAMESPACE}.svc.cluster.local:8200"
echo ""
return 0
fi
}
@ -144,7 +155,7 @@ fetch_openbao_secret() {
local token url
token=$(openbao_token)
url=$(openbao_url)
if [[ -z "$token" ]]; then
if [[ -z "$token" || -z "$url" ]]; then
echo ""
return 0
fi
@ -179,7 +190,7 @@ write_grafana_password_to_openbao() {
local token url
token=$(openbao_token)
url=$(openbao_url)
if [[ -z "$token" || -z "${GRAFANA_ADMIN_PASSWORD:-}" ]]; then
if [[ -z "$token" || -z "$url" || -z "${GRAFANA_ADMIN_PASSWORD:-}" ]]; then
return 0
fi
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
@ -285,6 +296,13 @@ install_monitoring() {
write_grafana_password_to_openbao
# Use --force-conflicts with Server-Side Apply (SSA) to handle potential conflicts during upgrade.
local grafana_persistence_enabled="true"
local grafana_storage_class="prole-iscsi"
if [[ "${PROLE_MODE:-}" == "k3d" ]]; then
grafana_persistence_enabled="false"
grafana_storage_class=""
fi
helm_upgrade_with_retry \
"$GRAFANA_RELEASE" \
"$NAMESPACE" \
@ -292,9 +310,9 @@ install_monitoring() {
--force-conflicts \
--server-side=true \
--set "rbac.namespaced=true" \
--set "persistence.enabled=true" \
--set "persistence.enabled=${grafana_persistence_enabled}" \
--set "persistence.size=5Gi" \
--set "persistence.storageClassName=prole-iscsi" \
$(if [[ -n "$grafana_storage_class" ]]; then printf '%s ' --set "persistence.storageClassName=${grafana_storage_class}"; fi) \
--set "service.port=3000" \
--set "datasources.datasources\.yaml.apiVersion=1" \
--set "datasources.datasources\.yaml.datasources[0].name=Prometheus" \

View File

@ -4,7 +4,7 @@ set -euo pipefail
# init_openbao.sh
# Purpose:
# - Run OpenBao as a local Docker container (dev mode) and store admin key pair
# - Run OpenBao inside Kubernetes and store admin key pair
# - Optionally generate Kerberos krb5.conf ConfigMap for external realm (legacy k8s flow)
# - Store secrets in OpenBao kv (admin keys, user keys, db password, optional TDE keys)
@ -26,6 +26,7 @@ NAMESPACE_OVERRIDE=""
# Defaults
OPENBAO_NAME=${OPENBAO_NAME:-openbao}
OPENBAO_IMAGE=${OPENBAO_IMAGE:-ghcr.io/openbao/openbao:latest}
OPENBAO_PORT_FORWARD_LOCAL=${OPENBAO_PORT_FORWARD_LOCAL:-18200}
# Support both PROLE_HOME/k8s and sibling k8s directory
if [[ -d "$SCRIPT_DIR/../k8s/openbao" ]]; then
@ -131,9 +132,6 @@ ensure_namespace() {
fi
}
ensure_docker() {
command -v docker >/dev/null || { echo "Missing required tool: docker" >&2; exit 1; }
}
ensure_admin_keypair() {
# Ensure admin keypair exists and a root token is available
@ -212,7 +210,44 @@ bao_service_url() {
echo "$PROLE_OPENBAO_URL"
return 0
fi
echo "http://127.0.0.1:18200"
echo "http://127.0.0.1:${OPENBAO_PORT_FORWARD_LOCAL}"
}
_OPENBAO_PF_PID=""
_OPENBAO_PF_STARTED=0
start_openbao_port_forward() {
local port="${OPENBAO_PORT_FORWARD_LOCAL}"
local url="http://127.0.0.1:${port}"
if curl -sS "$url/v1/sys/health" >/dev/null 2>&1; then
return 0
fi
kubectl -n "$OPENBAO_RESOURCE_NAMESPACE" port-forward "svc/${OPENBAO_NAME}" "${port}:8200" >/dev/null 2>&1 &
_OPENBAO_PF_PID=$!
_OPENBAO_PF_STARTED=1
for _ in {1..20}; do
if curl -sS "$url/v1/sys/health" >/dev/null 2>&1; then
return 0
fi
if ! kill -0 "$_OPENBAO_PF_PID" >/dev/null 2>&1; then
break
fi
sleep 1
done
return 1
}
stop_openbao_port_forward() {
if [[ "${_OPENBAO_PF_STARTED}" == "1" && -n "${_OPENBAO_PF_PID:-}" ]]; then
kill "$_OPENBAO_PF_PID" >/dev/null 2>&1 || true
wait "$_OPENBAO_PF_PID" >/dev/null 2>&1 || true
fi
_OPENBAO_PF_STARTED=0
_OPENBAO_PF_PID=""
}
wait_for_openbao_http() {
@ -310,7 +345,11 @@ should_apply_kerberos_configmap() {
apply_k8s() {
echo "Applying OpenBao manifest to namespace '$OPENBAO_RESOURCE_NAMESPACE' ..."
if [[ -f "$SCRIPT_DIR/../k8s/prole/openbao-statefulset.yaml" ]]; then
local use_statefulset=0
if [[ "${PROLE_MODE:-}" != "k3d" && -f "$SCRIPT_DIR/../k8s/prole/openbao-statefulset.yaml" ]]; then
use_statefulset=1
fi
if [[ "$use_statefulset" == "1" ]]; then
local output=""
if output=$(prole_render_manifest "$SCRIPT_DIR/../k8s/prole/openbao-statefulset.yaml" \
| kubectl apply --validate=false -n "$OPENBAO_RESOURCE_NAMESPACE" -f - 2>&1); then
@ -540,54 +579,6 @@ init_openbao_kv_and_store_admin_key() {
# Removed: prompt_admin_password_and_apply_secret (no AD admin secret required)
docker_start() {
echo "Starting local Docker container for OpenBao ..."
ensure_docker
# 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
local running existing host_ip
running=$(docker ps --filter "name=^/${OPENBAO_NAME}$" --format '{{.Names}}' | tr -d '\n')
if [[ "$running" == "$OPENBAO_NAME" ]]; then
echo "OpenBao container already running: $OPENBAO_NAME"
return 0
fi
existing=$(docker ps -a --filter "name=^/${OPENBAO_NAME}$" --format '{{.Names}}' | tr -d '\n')
if [[ "$existing" == "$OPENBAO_NAME" ]]; then
host_ip=$(docker inspect -f '{{(index (index .HostConfig.PortBindings "8200/tcp") 0).HostIp}}' "$OPENBAO_NAME" 2>/dev/null || echo "")
if [[ "$host_ip" == "127.0.0.1" || -z "$host_ip" ]]; then
echo "Starting existing OpenBao container: $OPENBAO_NAME"
docker start "$OPENBAO_NAME" >/dev/null
return 0
fi
echo "Existing OpenBao container is bound to $host_ip; recreating with 127.0.0.1..."
docker rm -f "$OPENBAO_NAME" >/dev/null 2>&1 || true
fi
docker run -d --name "$OPENBAO_NAME" \
--restart unless-stopped \
-p 127.0.0.1:18200:8200 \
"$OPENBAO_IMAGE" server -dev -dev-listen-address=0.0.0.0:8200 -dev-root-token-id="$token"
echo "Docker container started: $OPENBAO_NAME"
}
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 ---"
@ -643,16 +634,6 @@ cmd_status() {
fi
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 keypair present in $SECRETS_DIR"
@ -710,27 +691,54 @@ cmd_status() {
case "$ACTION" in
start)
ensure_tools
ensure_docker
ensure_admin_keypair
docker_start
ensure_k8s_tools
ensure_namespace
generate_openbao_manifests
if should_apply_kerberos_configmap; then
generate_kerberos_configmap
fi
k3d_cleanup_pending_openbao_pvc
apply_k8s
apply_openbao_host_network_patch
wait_for_openbao
prole_register_port_forward "openbao" "${OPENBAO_NAMESPACE:-default}" "svc/openbao" "8200" "8200" "127.0.0.1" "TCP" "OpenBao"
;;
status)
ensure_tools
cmd_status
;;
stop)
ensure_docker
docker_stop
ensure_k8s_tools
if kubectl get statefulset "$OPENBAO_NAME" -n "$OPENBAO_RESOURCE_NAMESPACE" >/dev/null 2>&1; then
kubectl scale statefulset "$OPENBAO_NAME" -n "$OPENBAO_RESOURCE_NAMESPACE" --replicas=0 >/dev/null 2>&1 || true
elif kubectl get deployment "$OPENBAO_NAME" -n "$OPENBAO_RESOURCE_NAMESPACE" >/dev/null 2>&1; then
kubectl scale deployment "$OPENBAO_NAME" -n "$OPENBAO_RESOURCE_NAMESPACE" --replicas=0 >/dev/null 2>&1 || true
fi
;;
restart)
ensure_tools
ensure_docker
ensure_admin_keypair
docker_restart
ensure_k8s_tools
if kubectl get statefulset "$OPENBAO_NAME" -n "$OPENBAO_RESOURCE_NAMESPACE" >/dev/null 2>&1; then
kubectl scale statefulset "$OPENBAO_NAME" -n "$OPENBAO_RESOURCE_NAMESPACE" --replicas=0 >/dev/null 2>&1 || true
kubectl scale statefulset "$OPENBAO_NAME" -n "$OPENBAO_RESOURCE_NAMESPACE" --replicas=1 >/dev/null 2>&1 || true
elif kubectl get deployment "$OPENBAO_NAME" -n "$OPENBAO_RESOURCE_NAMESPACE" >/dev/null 2>&1; then
kubectl scale deployment "$OPENBAO_NAME" -n "$OPENBAO_RESOURCE_NAMESPACE" --replicas=0 >/dev/null 2>&1 || true
kubectl scale deployment "$OPENBAO_NAME" -n "$OPENBAO_RESOURCE_NAMESPACE" --replicas=1 >/dev/null 2>&1 || true
fi
wait_for_openbao
;;
initialize)
ensure_tools
ensure_docker
ensure_k8s_tools
ensure_namespace
generate_openbao_manifests
if should_apply_kerberos_configmap; then
generate_kerberos_configmap
fi
k3d_cleanup_pending_openbao_pvc
apply_k8s
apply_openbao_host_network_patch
wait_for_openbao
# Read password from stdin if provided
db_pass=""
if [[ ! -t 0 ]]; then
@ -738,7 +746,11 @@ case "$ACTION" in
fi
ensure_admin_keypair
docker_start
trap stop_openbao_port_forward EXIT
if ! start_openbao_port_forward; then
echo "ERROR: Unable to establish OpenBao port-forward on 127.0.0.1:${OPENBAO_PORT_FORWARD_LOCAL}" >&2
exit 1
fi
wait_for_openbao_http 20
init_openbao_kv_and_store_admin_key
prole_register_port_forward "openbao" "${OPENBAO_NAMESPACE:-default}" "svc/openbao" "8200" "8200" "127.0.0.1" "TCP" "OpenBao"

View File

@ -101,10 +101,21 @@ ensure_namespace() {
openbao_url() {
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
echo "$PROLE_OPENBAO_URL"
return 0
fi
if prole_is_in_cluster; then
echo "http://openbao.${OPENTOFU_OPENBAO_NAMESPACE}.svc.cluster.local:8200"
return 0
fi
if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:8200"
return 0
elif 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
else
echo "http://openbao.${OPENTOFU_OPENBAO_NAMESPACE}.svc.cluster.local:8200"
echo ""
return 0
fi
}
@ -122,7 +133,7 @@ fetch_openbao_secret() {
local token url
token=$(openbao_token)
url=$(openbao_url)
if [[ -z "$token" ]]; then
if [[ -z "$token" || -z "$url" ]]; then
echo ""
return 0
fi

View File

@ -618,6 +618,25 @@ is_local_mode() {
pid_file_for() { printf '%s/%s.pid' "$PID_DIR" "$1"; }
log_file_for() { printf '%s/%s.log' "$LOG_DIR" "$1"; }
wait_for_service_endpoints() {
local ns="$1" svc="$2"
local timeout="${PORT_FORWARD_WAIT_TIMEOUT:-120}"
local interval="${PORT_FORWARD_WAIT_INTERVAL:-3}"
local start
start=$(date +%s)
while true; do
if "$KUBECTL" -n "$ns" get endpoints "$svc" -o jsonpath='{.subsets[*].addresses[*].ip}' 2>/dev/null | grep -q .; then
return 0
fi
if (( $(date +%s) - start > timeout )); then
err "WARN: Endpoints for service '$svc' in namespace '$ns' not ready after ${timeout}s; continuing."
return 1
fi
sleep "$interval"
done
}
is_pid_running() {
# Return 0 if pid exists and running, else 1
# kill -0 is portable.
@ -783,6 +802,11 @@ start_port_forward() {
# Aggressively stop existing processes before starting to avoid port conflicts
stop_port_forward "$id" "$ns" "$target" "$address" "$hostPort" "$servicePort" "$protocol" "$description"
# If target is a service, wait briefly for endpoints to be ready to avoid immediate port-forward failures.
if [[ "$target" == svc/* ]]; then
wait_for_service_endpoints "$ns" "${target#svc/}" || true
fi
pidfile="$(pid_file_for "$id")"
logfile="$(log_file_for "$id")"
cmd="$(build_port_forward_cmd "$ns" "$target" "$address" "$hostPort" "$servicePort")"

View File

@ -73,6 +73,17 @@ _prole_host_from_url() {
printf '%s' "$val"
}
prole_is_in_cluster() {
[[ -n "${KUBERNETES_SERVICE_HOST:-}" && -f /var/run/secrets/kubernetes.io/serviceaccount/token ]]
}
_prole_local_registry_enabled() {
case "${PROLE_ENABLE_LOCAL_REGISTRY:-${ENABLE_LOCAL_REGISTRY:-}}" in
1|true|TRUE|True|yes|YES|Yes|on|ON|On) return 0 ;;
esac
return 1
}
_PROLE_CFG_SET_VARS="|"
_prole_cfg_set_default() {
@ -246,22 +257,26 @@ if [[ -n "${NAMESPACE:-}" ]]; then
_prole_mode_resolved=$(prole_normalize_mode "${PROLE_MODE:-}")
case "$_prole_mode_resolved" in
k3d)
if [[ -z "${LOCAL_REGISTRY_INTERNAL:-}" ]]; then
if [[ -n "${LOCAL_REGISTRY_INTERNAL:-}" ]]; then
if [[ "${LOCAL_REGISTRY_INTERNAL}" == *.localhost:5000 ]]; then
LOCAL_REGISTRY_INTERNAL="${LOCAL_REGISTRY_INTERNAL%.localhost:5000}:5000"
export LOCAL_REGISTRY_INTERNAL
elif [[ "${LOCAL_REGISTRY_INTERNAL}" == *.localhost ]]; then
LOCAL_REGISTRY_INTERNAL="${LOCAL_REGISTRY_INTERNAL%.localhost}"
export LOCAL_REGISTRY_INTERNAL
fi
elif _prole_local_registry_enabled; then
LOCAL_REGISTRY_INTERNAL="k3d-prole-registry:5000"
export LOCAL_REGISTRY_INTERNAL
elif [[ "${LOCAL_REGISTRY_INTERNAL}" == *.localhost:5000 ]]; then
LOCAL_REGISTRY_INTERNAL="${LOCAL_REGISTRY_INTERNAL%.localhost:5000}:5000"
export LOCAL_REGISTRY_INTERNAL
elif [[ "${LOCAL_REGISTRY_INTERNAL}" == *.localhost ]]; then
LOCAL_REGISTRY_INTERNAL="${LOCAL_REGISTRY_INTERNAL%.localhost}"
export LOCAL_REGISTRY_INTERNAL
fi
;;
esac
case "$_prole_mode_resolved" in
k3s|k8s)
_prole_registry_ns="${SERVICE_NAMESPACE:-${NAMESPACE}}"
if [[ -z "${LOCAL_REGISTRY_INTERNAL:-}" || "${LOCAL_REGISTRY_INTERNAL}" == *"k3d"* ]]; then
if [[ -n "${LOCAL_REGISTRY_INTERNAL:-}" ]]; then
:
elif _prole_local_registry_enabled; then
_prole_registry_host="$(_prole_host_from_url "${PROLE_K3S_SERVER:-${K3S_SERVER_URL:-}}")"
if [[ -n "${_prole_registry_host:-}" ]]; then
LOCAL_REGISTRY_INTERNAL="${_prole_registry_host}:5000"

View File

@ -192,9 +192,13 @@ repair_dashboard() {
warn "Dashboard namespace missing; installing via Helm"
if have helm; then
helm repo add kubernetes-dashboard https://kubernetes.github.io/dashboard/ >/dev/null 2>&1 || true
helm repo update >/dev/null 2>&1 || true
helm upgrade --install kubernetes-dashboard kubernetes-dashboard/kubernetes-dashboard \
--create-namespace --namespace "$ns"
if ! run_with_timeout 120 bash -c 'helm repo update >/dev/null 2>&1'; then
warn "Helm repo update timed out; continuing with cached index"
fi
if ! run_with_timeout 300 helm upgrade --install kubernetes-dashboard kubernetes-dashboard/kubernetes-dashboard \
--create-namespace --namespace "$ns"; then
warn "Helm install timed out or failed"
fi
else
warn "helm not found; cannot install kubernetes-dashboard"
return 1
@ -206,8 +210,10 @@ repair_dashboard() {
if [[ -z "$kong_pods" ]]; then
warn "Dashboard Kong pod missing; re-applying Helm release"
if have helm; then
helm upgrade --install kubernetes-dashboard kubernetes-dashboard/kubernetes-dashboard \
--create-namespace --namespace "$ns"
if ! run_with_timeout 300 helm upgrade --install kubernetes-dashboard kubernetes-dashboard/kubernetes-dashboard \
--create-namespace --namespace "$ns"; then
warn "Helm install timed out or failed"
fi
else
warn "helm not found; cannot reinstall kubernetes-dashboard"
return 1

13239
install.py

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,10 @@ Modules:
- config: shared paths, dependency catalog, platform helpers, version utils.
- build: Prole app build command composition.
- deploy: host-side deploy helpers (Xcode tools check, app build flow).
- milestone: installer milestone abstraction.
- state: installer state model.
- runner: milestone runner with event callbacks.
- core: UI-agnostic installer core wrapper.
- screen: (optional) UI controller placement point.
The entry point remains at `prole/install.py` and the legacy root `install.py`
@ -18,5 +22,9 @@ __all__ = [
"config",
"build",
"deploy",
"milestone",
"state",
"runner",
"core",
"screen",
]

View File

@ -6,11 +6,10 @@ UI-agnostic command composition for the Prole macOS app build.
from __future__ import annotations
from pathlib import Path
import tkinter as tk
from tkinter import ttk, scrolledtext
from typing import Callable
from . import main as inst_main
from . import screen as ui
from .milestone import Milestone
from .state import InstallerState
def get_build_command(project_root: Path, env: str | None) -> str:
"""Return the Prole app build command with an env comment suffix.
@ -30,9 +29,38 @@ def get_build_command(project_root: Path, env: str | None) -> str:
return cmd
class BuildMilestone(Milestone):
"""UI-agnostic build milestone."""
def __init__(self, project_root: Path, env: str | None = None, next_id: str | None = None):
super().__init__("build", "Build")
self.project_root = Path(project_root)
self.env = env
self._next_id = next_id
def validate(self, state: InstallerState) -> list[str] | None:
if not self.project_root.exists():
return [f"Project root not found: {self.project_root}"]
return None
def execute(self, state: InstallerState, progress: Callable[[str, float | None], None] | None = None) -> None:
cmd = get_build_command(self.project_root, self.env)
state.data["build.command"] = cmd
if progress:
progress("Build command prepared", 1.0)
def next(self, state: InstallerState) -> str | None:
return self._next_id
# ---------------- Screen (UI) helpers ----------------
def create_build_page(app):
"""Create the Build page UI and register it via installer.main."""
import tkinter as tk
from tkinter import ttk, scrolledtext
from . import main as inst_main
f = ttk.Frame(app.page_area)
f.place(x=0, y=0, relwidth=1, relheight=1)

25
installer/core.py Normal file
View File

@ -0,0 +1,25 @@
"""UI-agnostic installer engine."""
from __future__ import annotations
from typing import Iterable
from .milestone import Milestone
from .runner import InstallerRunner
from .state import InstallerState
class InstallerCore:
"""Convenience wrapper around the runner and milestones."""
def __init__(self, milestones: Iterable[Milestone], state: InstallerState | None = None):
self.runner = InstallerRunner(milestones, state=state)
@property
def state(self) -> InstallerState:
return self.runner.state
def on(self, event: str, callback) -> None:
self.runner.on(event, callback)
def run(self, start_id: str | None = None) -> bool:
return self.runner.run(start_id=start_id)

View File

@ -14,11 +14,11 @@ import os
import threading
import platform
import subprocess
import tkinter as tk
from tkinter import ttk, messagebox
import shutil
from typing import Callable
from . import main as inst_main
from . import screen as ui
from .milestone import Milestone
from .state import InstallerState
from . import config as cfg
@ -32,7 +32,7 @@ def check_xcode_tools() -> bool:
return False
def build_prole_app(project_root: Path) -> None:
def build_prole_app_core(project_root: Path) -> Path:
if platform.system() != "Darwin":
raise Exception("Building the macOS app requires macOS.")
if not check_xcode_tools():
@ -54,7 +54,14 @@ def build_prole_app(project_root: Path) -> None:
if not app_path.exists():
raise Exception("Build completed but Prole.app was not found in dist/")
return app_path
def build_prole_app(project_root: Path) -> None:
"""Legacy UI flow for building and optionally copying Prole.app."""
app_path = build_prole_app_core(project_root)
try:
from tkinter import messagebox
msg = (
"Prole.app has been built successfully.\n\n"
f"Location: {app_path}\n\n"
@ -68,8 +75,56 @@ def build_prole_app(project_root: Path) -> None:
print(f"Copy to /Applications failed: {copy_err}")
class DeployMilestone(Milestone):
"""UI-agnostic deploy milestone."""
def __init__(
self,
project_root: Path,
next_id: str | None = None,
*,
build_app: bool = True,
copy_to_applications: bool = False,
) -> None:
super().__init__("deploy", "Deploy")
self.project_root = Path(project_root)
self._next_id = next_id
self.build_app = build_app
self.copy_to_applications = copy_to_applications
def validate(self, state: InstallerState) -> list[str] | None:
if self.build_app and platform.system() != "Darwin":
return ["Building the macOS app requires macOS."]
return None
def execute(self, state: InstallerState, progress: Callable[[str, float | None], None] | None = None) -> None:
if not self.build_app:
if progress:
progress("Deploy step skipped (build_app disabled).", 1.0)
return
if progress:
progress("Building Prole.app", 0.2)
app_path = build_prole_app_core(self.project_root)
state.data["deploy.app_path"] = str(app_path)
if self.copy_to_applications:
dest = Path("/Applications") / "Prole.app"
if dest.exists():
state.data["deploy.copy_skipped"] = f"Destination already exists: {dest}"
else:
shutil.copytree(app_path, dest)
state.data["deploy.copied_to"] = str(dest)
if progress:
progress("Deploy completed", 1.0)
def next(self, state: InstallerState) -> str | None:
return self._next_id
# ---------------- Screen (UI) helpers ----------------
def _draw_status(canvas: tk.Canvas, status: str):
def _draw_status(canvas, status: str):
canvas.delete('all')
if status == 'success' or status == 'completed':
canvas.create_oval(2, 2, 18, 18, fill='#34c759', outline='')
@ -84,6 +139,9 @@ def _draw_status(canvas: tk.Canvas, status: str):
def _create_deploy_row(app, parent, step: dict):
import tkinter as tk
from tkinter import ttk
row = ttk.Frame(parent)
row.pack(fill='x', pady=6)
canvas = tk.Canvas(row, width=20, height=20, highlightthickness=0)
@ -114,6 +172,12 @@ def create_deploy_page(app):
- Show completed steps from earlier phases
- Install Prole.app (drag-to-install pop-up)
"""
import tkinter as tk
from tkinter import ttk
from . import main as inst_main
from . import screen as ui
f = ttk.Frame(app.page_area)
f.place(x=0, y=0, relwidth=1, relheight=1)
@ -370,5 +434,3 @@ def _ensure_docker_running(timeout: int = 120) -> bool:
return True
_t.sleep(2)
return False

38
installer/milestone.py Normal file
View File

@ -0,0 +1,38 @@
"""Milestone abstraction for the UI-agnostic installer core."""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Callable, Iterable, Sequence
from .state import InstallerState
ProgressCallback = Callable[[str, float | None], None]
ValidationResult = str | Sequence[str] | None
class Milestone(ABC):
"""Base class for installer milestones.
Milestones encapsulate business logic, validation, and flow control.
They must not import or depend on UI toolkits.
"""
id: str
title: str
def __init__(self, milestone_id: str, title: str):
self.id = milestone_id
self.title = title
def validate(self, state: InstallerState) -> ValidationResult:
"""Return a validation error or None when valid."""
return None
@abstractmethod
def execute(self, state: InstallerState, progress: ProgressCallback | None = None) -> None:
"""Execute milestone logic. Use the progress callback to emit updates."""
raise NotImplementedError
def next(self, state: InstallerState) -> str | None:
"""Return the next milestone id or None to finish."""
return None

View File

@ -659,6 +659,37 @@ class ProleNcursesInstaller:
win.render_text(y, 4, f"Status: {status}")
class NcursesUIAdapter:
\"\"\"Adapter skeleton that matches the Tkinter UI event interface.\"\"\"
def __init__(self, runner, controller):
self.runner = runner
self.controller = controller
self._bind_runner()
def _bind_runner(self):
self.runner.on('on_enter_milestone', self.on_enter_milestone)
self.runner.on('on_validation_error', self.on_validation_error)
self.runner.on('on_progress', self.on_progress)
self.runner.on('on_complete', self.on_complete)
# Event handlers (override to bridge into curses rendering)
def on_enter_milestone(self, milestone, state):
pass
def on_validation_error(self, milestone, state, errors):
pass
def on_progress(self, milestone, state, message, percent):
pass
def on_complete(self, milestone, state):
pass
def start(self):
run_ncurses_installer(self.controller)
def run_ncurses_installer(controller):
"""Entry point for ncurses installer."""
def _main(stdscr):

84
installer/runner.py Normal file
View File

@ -0,0 +1,84 @@
"""Installer runner that executes milestones sequentially."""
from __future__ import annotations
from typing import Callable, Iterable
from .milestone import Milestone, ValidationResult
from .state import InstallerState
EventCallback = Callable[..., None]
class InstallerRunner:
"""Executes milestones sequentially and emits lifecycle events."""
def __init__(self, milestones: Iterable[Milestone], state: InstallerState | None = None):
self._milestones = {m.id: m for m in milestones}
self._order = [m.id for m in milestones]
self.state = state or InstallerState()
self._listeners: dict[str, list[EventCallback]] = {
'on_enter_milestone': [],
'on_validation_error': [],
'on_progress': [],
'on_complete': [],
}
def on(self, event: str, callback: EventCallback) -> None:
if event not in self._listeners:
raise ValueError(f"Unknown event: {event}")
self._listeners[event].append(callback)
def emit(self, event: str, *args) -> None:
for cb in self._listeners.get(event, []):
cb(*args)
def run(self, start_id: str | None = None) -> bool:
if not self._order:
self.emit('on_complete', None, self.state)
return True
current_id = start_id or self.state.current_id or self._order[0]
while current_id:
milestone = self._milestones.get(current_id)
if milestone is None:
self.state.errors.append(f"Unknown milestone id: {current_id}")
self.emit('on_validation_error', None, self.state, [self.state.errors[-1]])
return False
self.state.current_id = current_id
self.state.history.append(current_id)
self.emit('on_enter_milestone', milestone, self.state)
errors = milestone.validate(self.state)
error_list = _normalize_validation(errors)
if error_list:
self.state.errors.extend(error_list)
self.emit('on_validation_error', milestone, self.state, error_list)
return False
def _progress(message: str, percent: float | None = None) -> None:
if percent is not None:
self.state.progress[current_id] = percent
self.emit('on_progress', milestone, self.state, message, percent)
milestone.execute(self.state, _progress)
self.state.mark_completed(current_id)
next_id = milestone.next(self.state)
if not next_id:
self.emit('on_complete', milestone, self.state)
return True
current_id = next_id
self.emit('on_complete', None, self.state)
return True
def _normalize_validation(result: ValidationResult) -> list[str]:
if result is None:
return []
if isinstance(result, str):
return [result]
return [str(item) for item in result]

18
installer/state.py Normal file
View File

@ -0,0 +1,18 @@
"""State model for the UI-agnostic installer core."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
@dataclass
class InstallerState:
current_id: str | None = None
data: dict[str, Any] = field(default_factory=dict)
errors: list[str] = field(default_factory=list)
progress: dict[str, float] = field(default_factory=dict)
history: list[str] = field(default_factory=list)
completed: set[str] = field(default_factory=set)
def mark_completed(self, milestone_id: str) -> None:
self.completed.add(milestone_id)

1
installer/ui/__init__.py Normal file
View File

@ -0,0 +1 @@
"""UI adapters for the installer."""

13362
installer/ui/legacy_tk.py Executable file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
"""Ncurses UI adapter skeleton for the installer runner."""
from __future__ import annotations
from ..runner import InstallerRunner
from ..state import InstallerState
from ..milestone import Milestone
class NcursesUI:
"""Minimal ncurses adapter that listens to runner events."""
def __init__(self, runner: InstallerRunner):
self.runner = runner
self._bind_runner()
def _bind_runner(self) -> None:
self.runner.on('on_enter_milestone', self.on_enter_milestone)
self.runner.on('on_validation_error', self.on_validation_error)
self.runner.on('on_progress', self.on_progress)
self.runner.on('on_complete', self.on_complete)
# Event handlers (override in concrete UI)
def on_enter_milestone(self, milestone: Milestone, state: InstallerState) -> None:
pass
def on_validation_error(self, milestone: Milestone | None, state: InstallerState, errors: list[str]) -> None:
pass
def on_progress(self, milestone: Milestone, state: InstallerState, message: str, percent: float | None) -> None:
pass
def on_complete(self, milestone: Milestone | None, state: InstallerState) -> None:
pass
def start(self, start_id: str | None = None) -> bool:
"""Run the installer without a GUI main loop."""
return self.runner.run(start_id=start_id)

View File

@ -0,0 +1,42 @@
"""Tkinter UI adapter skeleton for the installer runner."""
from __future__ import annotations
import threading
import tkinter as tk
from ..runner import InstallerRunner
from ..state import InstallerState
from ..milestone import Milestone
class TkinterUI:
"""Minimal Tkinter adapter that listens to runner events."""
def __init__(self, runner: InstallerRunner, root: tk.Tk | None = None):
self.runner = runner
self.root = root or tk.Tk()
self._bind_runner()
def _bind_runner(self) -> None:
self.runner.on('on_enter_milestone', self.on_enter_milestone)
self.runner.on('on_validation_error', self.on_validation_error)
self.runner.on('on_progress', self.on_progress)
self.runner.on('on_complete', self.on_complete)
# Event handlers (override in concrete UI)
def on_enter_milestone(self, milestone: Milestone, state: InstallerState) -> None:
pass
def on_validation_error(self, milestone: Milestone | None, state: InstallerState, errors: list[str]) -> None:
pass
def on_progress(self, milestone: Milestone, state: InstallerState, message: str, percent: float | None) -> None:
pass
def on_complete(self, milestone: Milestone | None, state: InstallerState) -> None:
pass
def start(self, start_id: str | None = None) -> None:
"""Start the runner in a background thread and enter Tk mainloop."""
threading.Thread(target=self.runner.run, kwargs={'start_id': start_id}, daemon=True).start()
self.root.mainloop()

View File

@ -17,6 +17,7 @@ spec:
containers:
- name: openbao
image: ghcr.io/openbao/openbao:latest
imagePullPolicy: IfNotPresent
args:
- "server"
- "-dev"