mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 12:03:59 +00:00
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:
parent
d2efb835b0
commit
6b89ea23d7
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
2
env.sh
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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" \
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
13239
install.py
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
]
|
||||
|
||||
@ -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
25
installer/core.py
Normal 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)
|
||||
@ -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
38
installer/milestone.py
Normal 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
|
||||
@ -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
84
installer/runner.py
Normal 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
18
installer/state.py
Normal 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
1
installer/ui/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""UI adapters for the installer."""
|
||||
13362
installer/ui/legacy_tk.py
Executable file
13362
installer/ui/legacy_tk.py
Executable file
File diff suppressed because it is too large
Load Diff
37
installer/ui/ncurses_ui.py
Normal file
37
installer/ui/ncurses_ui.py
Normal 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)
|
||||
42
installer/ui/tkinter_ui.py
Normal file
42
installer/ui/tkinter_ui.py
Normal 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()
|
||||
@ -17,6 +17,7 @@ spec:
|
||||
containers:
|
||||
- name: openbao
|
||||
image: ghcr.io/openbao/openbao:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- "server"
|
||||
- "-dev"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user