mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 10:13:58 +00:00
1394 lines
44 KiB
Bash
Executable File
1394 lines
44 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# knoe.sh - Unified launcher for Knoe infrastructure and installer
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
KNOE_HOME="${KNOE_HOME:-$ROOT_DIR}"
|
|
cd "${ROOT_DIR}"
|
|
|
|
if [[ -z "${OPENBAO_ROOT_TOKEN:-}" ]]; then
|
|
if [[ -f "${ROOT_DIR}/etc/secrets/openbao-root-token" ]]; then
|
|
export OPENBAO_ROOT_TOKEN=$(cat "${ROOT_DIR}/etc/secrets/openbao-root-token")
|
|
fi
|
|
fi
|
|
|
|
# Defaults
|
|
VERBOSE=0
|
|
DEBUG=0
|
|
LOG=0
|
|
COVERAGE=0
|
|
SILENT=0
|
|
RESET=0
|
|
EXTRA_ARGS=()
|
|
|
|
# Canonical wrapper aliases:
|
|
# ./knoe.sh --reset => full silent reset run
|
|
# ./knoe.sh --update => silent update run
|
|
# ./knoe.sh --perfsnap => topology/storage snapshot
|
|
if [[ $# -gt 0 ]]; then
|
|
case "$1" in
|
|
--reset)
|
|
set -- reset-run "${@:2}"
|
|
;;
|
|
--update)
|
|
set -- update "${@:2}"
|
|
;;
|
|
--perfsnap)
|
|
set -- perfsnap "${@:2}"
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
usage() {
|
|
echo "Usage: $0 [options] [command] [command-options]"
|
|
echo
|
|
echo "Options:"
|
|
echo " -l, --log Create a datestamped logfile in logs including all stdout/stderr"
|
|
echo " -v, --verbose Pass a verbose switch to all functions called by ansible and install.sh"
|
|
echo " -d, --debug Enable a debug logfile in logs with high verbosity from install.sh"
|
|
echo " -c, --config Path to config file (default: conf/{k3d|k3s|gke|min}.cfg)"
|
|
echo " --coverage Create a coverage report for the run of install.sh"
|
|
echo " -s, --silent Run unattended install (passes -S to install.sh)"
|
|
echo " -r, --reset Perform a reset before action (site/deploy/install); for canonical full reset run use './knoe.sh --reset'"
|
|
echo " --min Use minimal containerd mode"
|
|
echo
|
|
echo "Commands:"
|
|
echo " init Run/re-run initialization scripts to verify, repair, and renew the deployment"
|
|
echo " install Run the ncurses-based terminal installer"
|
|
echo " ansible Run ansible-playbook via ansible.sh"
|
|
echo " site Shortcut for running the site deployment"
|
|
echo " reset Shortcut for k3s factory reset (full clear)"
|
|
echo " reset-run Canonical full reset run (equivalent to legacy '-r -s -v -l')"
|
|
echo " update Canonical silent update run (installer --update)"
|
|
echo " perfsnap Save topology/storage performance snapshot (includes storage_probe)"
|
|
echo " deploy Full end-to-end: site run, and silent install (reset optional)"
|
|
echo " start Launch services (containerd in min mode, or cluster in k3d/k3s/k8s)"
|
|
echo " stop Stop services"
|
|
echo " restart Restart services (min mode only)"
|
|
echo " backup Run a database backup (-f/full for full, default incremental)"
|
|
echo " passwd Change the Knoe master password and propagate secrets"
|
|
echo " status Show deployment health status (use -v for verbose)"
|
|
echo
|
|
echo "Any other arguments are passed directly to ansible.sh"
|
|
}
|
|
|
|
# Parse global options
|
|
MIN_MODE=0
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-l|--log|--logs) LOG=1; shift ;;
|
|
-v|--verbose) VERBOSE=1; shift ;;
|
|
-d|--debug) DEBUG=1; shift ;;
|
|
-c|--config) CONFIG_PATH="$2"; shift 2 ;;
|
|
--coverage) COVERAGE=1; shift ;;
|
|
-s|--silent) SILENT=1; shift ;;
|
|
-r|--reset) RESET=1; shift ;;
|
|
--min) MIN_MODE=1; EXTRA_ARGS+=("--min"); shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
install|ansible|site|reset|reset-run|update|perfsnap|deploy|init|start|stop|restart|backup|passwd|status) break ;;
|
|
*) EXTRA_ARGS+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
if [[ $# -eq 0 && ${#EXTRA_ARGS[@]} -eq 0 ]]; then
|
|
if [[ "${SILENT}" -eq 1 ]]; then
|
|
set -- install
|
|
else
|
|
usage
|
|
exit 1
|
|
fi
|
|
fi
|
|
CMD="${1:-install}"
|
|
[[ $# -gt 0 ]] && shift
|
|
|
|
# Prepare command arguments
|
|
CMD_ARGS=()
|
|
if [[ "${VERBOSE}" -eq 1 ]]; then
|
|
CMD_ARGS+=("--verbose")
|
|
export PROLE_VERBOSE=1
|
|
export VERBOSE=1
|
|
export CLICOLOR_FORCE=1
|
|
export PY_COLORS=1
|
|
export ANSIBLE_FORCE_COLOR=1
|
|
fi
|
|
|
|
# We separate arguments that are only for install.sh
|
|
INSTALL_ONLY_ARGS=()
|
|
if [[ "${DEBUG}" -eq 1 ]]; then
|
|
INSTALL_ONLY_ARGS+=("--debug")
|
|
fi
|
|
|
|
default_cfg_path() {
|
|
local conf_dir mode_hint mode cfg
|
|
conf_dir="${KNOE_CONF:-${ROOT_DIR}/conf}"
|
|
mode_hint="${KNOE_MODE:-${DEPLOYMENT_MODE:-${CLUSTER_ENV:-}}}"
|
|
mode="${mode_hint,,}"
|
|
|
|
case "$mode" in
|
|
min) [[ -f "$conf_dir/min.cfg" ]] && { printf '%s' "$conf_dir/min.cfg"; return 0; } ;;
|
|
k3d) [[ -f "$conf_dir/k3d.cfg" ]] && { printf '%s' "$conf_dir/k3d.cfg"; return 0; } ;;
|
|
k3s|service|services) [[ -f "$conf_dir/k3s.cfg" ]] && { printf '%s' "$conf_dir/k3s.cfg"; return 0; } ;;
|
|
k8s|gke|prod|production) [[ -f "$conf_dir/gke.cfg" ]] && { printf '%s' "$conf_dir/gke.cfg"; return 0; } ;;
|
|
esac
|
|
|
|
for cfg in min.cfg k3d.cfg k3s.cfg gke.cfg knoe.cfg; do
|
|
if [[ -f "$conf_dir/$cfg" ]]; then
|
|
printf '%s' "$conf_dir/$cfg"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
printf '%s' "$conf_dir/k3d.cfg"
|
|
}
|
|
|
|
if [[ "${SILENT}" -eq 1 ]]; then
|
|
_cfg="${CONFIG_PATH:-$(default_cfg_path)}"
|
|
if [[ ! -f "${_cfg}" ]]; then
|
|
echo "Error: --silent requires ${_cfg}" >&2
|
|
exit 1
|
|
fi
|
|
INSTALL_ONLY_ARGS+=("-S" "-c" "${_cfg}")
|
|
fi
|
|
if [[ -n "${CONFIG_PATH:-}" && "${SILENT}" -eq 0 ]]; then
|
|
INSTALL_ONLY_ARGS+=("-c" "${CONFIG_PATH}")
|
|
fi
|
|
if [[ "${RESET}" -eq 1 ]]; then
|
|
INSTALL_ONLY_ARGS+=("--reset")
|
|
fi
|
|
if [[ "${LOG}" -eq 1 ]]; then
|
|
INSTALL_ONLY_ARGS+=("--log")
|
|
fi
|
|
|
|
CMD_ARGS+=("${EXTRA_ARGS[@]}" "$@")
|
|
|
|
# Helper for logging
|
|
run_with_log() {
|
|
local logfile="$1"
|
|
shift
|
|
mkdir -p "$(dirname "${logfile}")"
|
|
echo "Logging to ${logfile}"
|
|
"$@" 2>&1 | tee "${logfile}"
|
|
}
|
|
|
|
# Ensure ansible keeps color output even when piped.
|
|
prepare_ansible_color() {
|
|
if [[ -z "${TERM:-}" ]]; then
|
|
export TERM="xterm-256color"
|
|
fi
|
|
unset ANSIBLE_NOCOLOR
|
|
export ANSIBLE_FORCE_COLOR=1
|
|
export PY_COLORS=1
|
|
export CLICOLOR_FORCE=1
|
|
}
|
|
|
|
PROLE_CFG_DEFAULT="$(default_cfg_path)"
|
|
|
|
resolve_cfg_path() {
|
|
if [[ -n "${CONFIG_PATH:-}" ]]; then
|
|
printf '%s' "${CONFIG_PATH}"
|
|
return 0
|
|
fi
|
|
printf '%s' "$(default_cfg_path)"
|
|
}
|
|
|
|
log_msg() {
|
|
printf '%s\n' "$*"
|
|
}
|
|
|
|
err_msg() {
|
|
printf '[error] %s\n' "$*" >&2
|
|
}
|
|
|
|
load_knoe_cfg() {
|
|
local cfg="${1:-$PROLE_CFG_DEFAULT}"
|
|
# Set KNOE_CONF before sourcing knoe_cfg.sh so it is preserved (knoe_cfg.sh
|
|
# honours a pre-set KNOE_CONF) and the correct config file is auto-detected.
|
|
if [[ -f "$cfg" ]]; then
|
|
KNOE_CONF="$(cd "$(dirname "$cfg")" && pwd)"
|
|
export KNOE_CONF
|
|
PROLE_CFG_FILE="$cfg"
|
|
export PROLE_CFG_FILE
|
|
fi
|
|
if [[ -f "${ROOT_DIR}/etc/knoe_cfg.sh" ]]; then
|
|
# shellcheck disable=SC1090
|
|
source "${ROOT_DIR}/etc/knoe_cfg.sh"
|
|
fi
|
|
export PROLE_CFG_FILE="${cfg}"
|
|
}
|
|
|
|
resolve_knoe_mode() {
|
|
local raw="${KNOE_MODE:-${DEPLOYMENT_MODE:-${CLUSTER_ENV:-}}}"
|
|
if command -v knoe_normalize_mode >/dev/null 2>&1; then
|
|
knoe_normalize_mode "${raw:-}"
|
|
else
|
|
printf '%s' "${raw:-}"
|
|
fi
|
|
}
|
|
|
|
ensure_openbao_url_for_mode() {
|
|
local mode="${1:-}"
|
|
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
case "$mode" in
|
|
k3d)
|
|
if curl -s -f "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then
|
|
export PROLE_OPENBAO_URL="http://127.0.0.1:8200"
|
|
elif curl -s -f "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then
|
|
export PROLE_OPENBAO_URL="http://127.0.0.1:18200"
|
|
fi
|
|
;;
|
|
k3s|k8s)
|
|
# In k3s/k8s, the installer runs outside the cluster and must reach OpenBao via a real
|
|
# externally reachable endpoint (no kubectl port-forward / localhost defaults).
|
|
if command -v _knoe_host_from_url >/dev/null 2>&1; then
|
|
local host
|
|
host=$(_knoe_host_from_url "${PROLE_K3S_SERVER:-${K3S_SERVER_URL:-}}")
|
|
if [[ -n "${host:-}" ]]; then
|
|
export PROLE_OPENBAO_URL="http://${host}:8200"
|
|
fi
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
resolve_k3d_cluster_name() {
|
|
local name="${DEPLOYMENT_TARGET:-${DISPLAY_NAME:-}}"
|
|
if [[ -z "$name" && -n "${KUBECTL_CONTEXT:-}" ]]; then
|
|
if [[ "${KUBECTL_CONTEXT}" == k3d-* ]]; then
|
|
name="${KUBECTL_CONTEXT#k3d-}"
|
|
fi
|
|
fi
|
|
if [[ -z "$name" && -n "${CLUSTER_ENV:-}" ]]; then
|
|
if [[ "${CLUSTER_ENV}" == k3d-* ]]; then
|
|
name="${CLUSTER_ENV#k3d-}"
|
|
else
|
|
name="${CLUSTER_ENV}"
|
|
fi
|
|
fi
|
|
printf '%s' "${name:-knoe-dev-cluster}"
|
|
}
|
|
|
|
fix_k3d_kubeconfig_server() {
|
|
local kubeconfig="$1"
|
|
local context="$2"
|
|
local desired_server="https://0.0.0.0:6443"
|
|
if [[ -z "$kubeconfig" || ! -f "$kubeconfig" ]]; then
|
|
return 0
|
|
fi
|
|
if ! kubectl --kubeconfig "$kubeconfig" config view >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
local ctx_cluster=""
|
|
ctx_cluster=$(kubectl --kubeconfig "$kubeconfig" config view -o jsonpath="{.contexts[?(@.name=='${context}')].context.cluster}" 2>/dev/null || true)
|
|
if [[ -z "$ctx_cluster" ]]; then
|
|
ctx_cluster="$context"
|
|
fi
|
|
local server=""
|
|
server=$(kubectl --kubeconfig "$kubeconfig" config view -o jsonpath="{.clusters[?(@.name=='${ctx_cluster}')].cluster.server}" 2>/dev/null || true)
|
|
if [[ -z "$server" || "$server" != "https://0.0.0.0:6443" ]]; then
|
|
kubectl --kubeconfig "$kubeconfig" config set-cluster "${ctx_cluster}" --server="${desired_server}" >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
|
|
ensure_tools() {
|
|
local mode="$1"
|
|
case "$mode" in
|
|
k3d)
|
|
command -v k3d >/dev/null 2>&1 || { err_msg "Missing required tool: k3d"; exit 1; }
|
|
command -v docker >/dev/null 2>&1 || { err_msg "Missing required tool: docker"; exit 1; }
|
|
command -v kubectl >/dev/null 2>&1 || { err_msg "Missing required tool: kubectl"; exit 1; }
|
|
;;
|
|
k3s|k8s)
|
|
command -v kubectl >/dev/null 2>&1 || { err_msg "Missing required tool: kubectl"; exit 1; }
|
|
;;
|
|
esac
|
|
}
|
|
|
|
stop_port_forwards() {
|
|
if [[ -x "${ROOT_DIR}/etc/init_port_forwards.sh" ]]; then
|
|
log_msg "Stopping port forwards..."
|
|
"${ROOT_DIR}/etc/init_port_forwards.sh" -c "${PROLE_CFG_FILE}" stop >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
|
|
ensure_k3d_cluster_ready() {
|
|
local cluster_name="$1"
|
|
ensure_tools "k3d"
|
|
if [[ -x "${ROOT_DIR}/etc/init_k8s.sh" ]]; then
|
|
"${ROOT_DIR}/etc/init_k8s.sh" -m k3d -n "${cluster_name}" initialize
|
|
"${ROOT_DIR}/etc/init_k8s.sh" -m k3d -n "${cluster_name}" start
|
|
else
|
|
err_msg "Missing ${ROOT_DIR}/etc/init_k8s.sh"
|
|
exit 1
|
|
fi
|
|
local context="${KUBECTL_CONTEXT:-k3d-${cluster_name}}"
|
|
k3d kubeconfig merge "${cluster_name}" --kubeconfig-switch-context >/dev/null 2>&1 || true
|
|
|
|
# Normalize the default kubeconfig to point at the LAN-friendly port.
|
|
if [[ -f "${HOME}/.kube/config" ]]; then
|
|
fix_k3d_kubeconfig_server "${HOME}/.kube/config" "${context}"
|
|
fi
|
|
|
|
local k3d_kubeconfig=""
|
|
k3d_kubeconfig="$(mktemp)"
|
|
if k3d kubeconfig get "${cluster_name}" > "${k3d_kubeconfig}" 2>/dev/null; then
|
|
export KUBECONFIG="${k3d_kubeconfig}"
|
|
kubectl --kubeconfig "${k3d_kubeconfig}" config use-context "${context}" >/dev/null 2>&1 || true
|
|
fix_k3d_kubeconfig_server "${k3d_kubeconfig}" "${context}"
|
|
else
|
|
rm -f "${k3d_kubeconfig}" >/dev/null 2>&1 || true
|
|
k3d_kubeconfig=""
|
|
fi
|
|
|
|
local attempts=60
|
|
local i
|
|
for i in $(seq 1 "$attempts"); do
|
|
if kubectl --context "${context}" get nodes >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
if [[ -n "${k3d_kubeconfig}" ]] && kubectl --kubeconfig "${k3d_kubeconfig}" get nodes >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
if (( i % 5 == 0 )); then
|
|
k3d kubeconfig merge "${cluster_name}" --kubeconfig-switch-context >/dev/null 2>&1 || true
|
|
if [[ -f "${HOME}/.kube/config" ]]; then
|
|
fix_k3d_kubeconfig_server "${HOME}/.kube/config" "${context}"
|
|
fi
|
|
if [[ -z "${k3d_kubeconfig}" ]]; then
|
|
k3d_kubeconfig="$(mktemp)"
|
|
if k3d kubeconfig get "${cluster_name}" > "${k3d_kubeconfig}" 2>/dev/null; then
|
|
export KUBECONFIG="${k3d_kubeconfig}"
|
|
fix_k3d_kubeconfig_server "${k3d_kubeconfig}" "${context}"
|
|
else
|
|
rm -f "${k3d_kubeconfig}" >/dev/null 2>&1 || true
|
|
k3d_kubeconfig=""
|
|
fi
|
|
fi
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
err_msg "k3d cluster '${cluster_name}' not reachable after ${attempts} attempts"
|
|
exit 2
|
|
}
|
|
|
|
ensure_k3s_cluster_ready() {
|
|
ensure_tools "k3s"
|
|
if command -v knoe_ensure_kubeconfig >/dev/null 2>&1; then
|
|
knoe_ensure_kubeconfig >/dev/null 2>&1 || true
|
|
fi
|
|
if ! kubectl get nodes >/dev/null 2>&1; then
|
|
err_msg "k3s cluster not reachable (kubectl get nodes failed)"
|
|
exit 2
|
|
fi
|
|
}
|
|
|
|
filter_install_args() {
|
|
local out=()
|
|
local arg
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
-s|-S|--silent|-c|--config|--no-gui|--gui) ;;
|
|
*) out+=("$arg") ;;
|
|
esac
|
|
done
|
|
printf '%s\0' "${out[@]}"
|
|
}
|
|
|
|
wait_for_backup() {
|
|
local ns="$1" backup_name="$2" timeout="${3:-1800}"
|
|
local start_time now phase phase_lc
|
|
start_time=$(date +%s)
|
|
while true; do
|
|
phase=$(kubectl -n "$ns" get backup "$backup_name" -o jsonpath='{.status.phase}' 2>/dev/null || true)
|
|
phase_lc=$(printf '%s' "$phase" | tr '[:upper:]' '[:lower:]')
|
|
case "$phase_lc" in
|
|
completed|succeeded)
|
|
log_msg "Backup ${backup_name} completed."
|
|
return 0
|
|
;;
|
|
failed|error)
|
|
err_msg "Backup ${backup_name} failed (phase=${phase})."
|
|
return 1
|
|
;;
|
|
esac
|
|
now=$(date +%s)
|
|
if (( now - start_time > timeout )); then
|
|
err_msg "Timed out waiting for backup ${backup_name}."
|
|
return 1
|
|
fi
|
|
log_msg "Waiting for backup ${backup_name} to complete (phase=${phase:-unknown}) ..."
|
|
sleep 10
|
|
done
|
|
}
|
|
|
|
start_garage_port_forward() {
|
|
local ns="$1"
|
|
local svc="$2"
|
|
local local_port="$3"
|
|
local pid_var="$4"
|
|
local port_open="0"
|
|
if python3 - "$local_port" <<'PY' >/dev/null 2>&1; then
|
|
import socket, sys
|
|
port=int(sys.argv[1])
|
|
s=socket.socket()
|
|
s.settimeout(0.2)
|
|
try:
|
|
s.connect(("127.0.0.1", port))
|
|
sys.exit(0)
|
|
except Exception:
|
|
sys.exit(1)
|
|
finally:
|
|
s.close()
|
|
PY
|
|
port_open="1"
|
|
fi
|
|
|
|
if [[ "$port_open" == "1" ]]; then
|
|
printf -v "$pid_var" '%s' ""
|
|
return 0
|
|
fi
|
|
|
|
kubectl -n "$ns" port-forward "svc/${svc}" "${local_port}:3900" >/dev/null 2>&1 &
|
|
local pf_pid=$!
|
|
printf -v "$pid_var" '%s' "$pf_pid"
|
|
|
|
local i
|
|
for i in {1..30}; do
|
|
if python3 - "$local_port" <<'PY' >/dev/null 2>&1; then
|
|
import socket, sys
|
|
port=int(sys.argv[1])
|
|
s=socket.socket()
|
|
s.settimeout(0.2)
|
|
try:
|
|
s.connect(("127.0.0.1", port))
|
|
sys.exit(0)
|
|
except Exception:
|
|
sys.exit(1)
|
|
finally:
|
|
s.close()
|
|
PY
|
|
return 0
|
|
fi
|
|
sleep 0.5
|
|
done
|
|
err_msg "Garage port-forward failed to come up on 127.0.0.1:${local_port}"
|
|
return 1
|
|
}
|
|
|
|
archive_backup_from_garage() {
|
|
local ns="$1"
|
|
local backup_name="$2"
|
|
local archive_root="$3"
|
|
local garage_ns="${GARAGE_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-default}}}"
|
|
local garage_svc="${GARAGE_NAME:-garage}"
|
|
local bucket="${GARAGE_BACKUP_BUCKET:-knoe-db-backups}"
|
|
local secret="${GARAGE_BACKUP_SECRET_NAME:-knoe-db-barman-s3}"
|
|
|
|
local access_key secret_key region
|
|
access_key=$(kubectl -n "$ns" get secret "$secret" -o jsonpath='{.data.ACCESS_KEY_ID}' 2>/dev/null | base64 -d || true)
|
|
secret_key=$(kubectl -n "$ns" get secret "$secret" -o jsonpath='{.data.SECRET_ACCESS_KEY}' 2>/dev/null | base64 -d || true)
|
|
region=$(kubectl -n "$ns" get secret "$secret" -o jsonpath='{.data.REGION}' 2>/dev/null | base64 -d || true)
|
|
|
|
if [[ -z "$access_key" || -z "$secret_key" ]]; then
|
|
err_msg "Unable to read Garage backup credentials from secret '$secret' in namespace '$ns'"
|
|
return 1
|
|
fi
|
|
if [[ -z "$region" ]]; then
|
|
region="garage"
|
|
fi
|
|
|
|
local dest_path backup_id server_name
|
|
dest_path=$(kubectl -n "$ns" get backup "$backup_name" -o jsonpath='{.status.destinationPath}' 2>/dev/null || true)
|
|
backup_id=$(kubectl -n "$ns" get backup "$backup_name" -o jsonpath='{.status.backupName}' 2>/dev/null || true)
|
|
server_name=$(kubectl -n "$ns" get backup "$backup_name" -o jsonpath='{.status.serverName}' 2>/dev/null || true)
|
|
|
|
local prefix=""
|
|
if [[ -n "$dest_path" && "$dest_path" == s3://* ]]; then
|
|
local rest="${dest_path#s3://}"
|
|
bucket="${rest%%/*}"
|
|
prefix="${rest#*/}"
|
|
if [[ "$prefix" == "$rest" ]]; then
|
|
prefix=""
|
|
fi
|
|
fi
|
|
if [[ -z "$backup_id" ]]; then
|
|
backup_id="$backup_name"
|
|
fi
|
|
if [[ -z "$prefix" ]]; then
|
|
if [[ -n "$server_name" && "$server_name" != "$backup_id" ]]; then
|
|
prefix="${server_name}/"
|
|
fi
|
|
fi
|
|
|
|
local contains="$backup_id"
|
|
if [[ -n "$prefix" && -n "$backup_id" && "$prefix" == *"$backup_id"* ]]; then
|
|
contains=""
|
|
fi
|
|
local archive_target="${archive_root}/${backup_name}"
|
|
local staging_dir="${archive_target}.partial"
|
|
|
|
mkdir -p "$archive_root"
|
|
rm -rf "$staging_dir"
|
|
mkdir -p "$staging_dir"
|
|
|
|
local pf_pid=""
|
|
start_garage_port_forward "$garage_ns" "$garage_svc" "3900" pf_pid
|
|
trap '[[ -n "${pf_pid:-}" ]] && kill "${pf_pid}" >/dev/null 2>&1 || true' EXIT
|
|
|
|
if ! python3 "${ROOT_DIR}/etc/archive_garage_backup.py" \
|
|
--endpoint "http://127.0.0.1:3900" \
|
|
--region "$region" \
|
|
--access-key "$access_key" \
|
|
--secret-key "$secret_key" \
|
|
--bucket "$bucket" \
|
|
--prefix "$prefix" \
|
|
--contains "$contains" \
|
|
--dest "$staging_dir"; then
|
|
if [[ -n "$contains" ]]; then
|
|
log_msg "No objects matched contains filter; retrying with prefix only..."
|
|
python3 "${ROOT_DIR}/etc/archive_garage_backup.py" \
|
|
--endpoint "http://127.0.0.1:3900" \
|
|
--region "$region" \
|
|
--access-key "$access_key" \
|
|
--secret-key "$secret_key" \
|
|
--bucket "$bucket" \
|
|
--prefix "$prefix" \
|
|
--contains "" \
|
|
--dest "$staging_dir"
|
|
else
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
rm -rf "$archive_target"
|
|
mv "$staging_dir" "$archive_target"
|
|
log_msg "Archived backup to ${archive_target}"
|
|
|
|
if [[ -n "${pf_pid:-}" ]]; then
|
|
kill "${pf_pid}" >/dev/null 2>&1 || true
|
|
wait "${pf_pid}" >/dev/null 2>&1 || true
|
|
fi
|
|
trap - EXIT
|
|
}
|
|
|
|
stop_monitoring_pods() {
|
|
local ns="${MONITORING_NAMESPACE:-monitoring}"
|
|
if ! kubectl get namespace "$ns" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
log_msg "Stopping monitoring pods in namespace ${ns} ..."
|
|
kubectl -n "$ns" scale deploy --all --replicas=0 >/dev/null 2>&1 || true
|
|
kubectl -n "$ns" scale statefulset --all --replicas=0 >/dev/null 2>&1 || true
|
|
kubectl -n "$ns" delete daemonset --all --ignore-not-found >/dev/null 2>&1 || true
|
|
kubectl -n "$ns" delete pod --all --ignore-not-found >/dev/null 2>&1 || true
|
|
}
|
|
|
|
# Run command
|
|
TS="$(date +%Y%m%d-%H%M%S)"
|
|
|
|
# Prep for Ansible truthfulness
|
|
if [[ -f "${ROOT_DIR}/etc/sync-knoe-cfg.py" ]]; then
|
|
# We use the same vault pass logic as ansible.sh if available
|
|
export PYTHONPATH="${PYTHONPATH:-}:${ROOT_DIR}"
|
|
python3 "${ROOT_DIR}/etc/sync-knoe-cfg.py" || true
|
|
fi
|
|
|
|
case "${CMD}" in
|
|
reset-run)
|
|
CONFIG_PATH="$(resolve_cfg_path)"
|
|
_reset_args=("install" "-s" "-v" "-l" "-r" "-c" "${CONFIG_PATH}")
|
|
if [[ "${COVERAGE}" -eq 1 ]]; then
|
|
_reset_args=("--coverage" "${_reset_args[@]}")
|
|
fi
|
|
exec "${ROOT_DIR}/knoe.sh" "${_reset_args[@]}"
|
|
;;
|
|
update)
|
|
CONFIG_PATH="$(resolve_cfg_path)"
|
|
_update_args=("install" "-s" "-v" "-l" "-c" "${CONFIG_PATH}" "--update")
|
|
if [[ "${COVERAGE}" -eq 1 ]]; then
|
|
_update_args=("--coverage" "${_update_args[@]}")
|
|
fi
|
|
exec "${ROOT_DIR}/knoe.sh" "${_update_args[@]}"
|
|
;;
|
|
perfsnap)
|
|
CONFIG_PATH="$(resolve_cfg_path)"
|
|
load_knoe_cfg "${CONFIG_PATH}"
|
|
_mode_for_env=$(resolve_knoe_mode)
|
|
if [[ -z "${_mode_for_env:-}" ]]; then
|
|
_mode_for_env="k3d"
|
|
fi
|
|
ensure_openbao_url_for_mode "${_mode_for_env}"
|
|
mkdir -p "${ROOT_DIR}/logs" "${ROOT_DIR}/data/topology"
|
|
_snap_ts="$(date +%Y%m%d-%H%M%S)"
|
|
_snap_file="${ROOT_DIR}/logs/perfsnap-${_snap_ts}.json"
|
|
export PROLE_PERFSNAP_FILE="${_snap_file}"
|
|
export PROLE_PERFSNAP_CFG="${CONFIG_PATH}"
|
|
python3 - <<'PY'
|
|
import json
|
|
import os
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
from knoe import knoe_conf
|
|
from knoe.core.env import _kubectl_base_cmd_for_k3s
|
|
from knoe.core.topology import TopologyDiscoveryConfig, discover_cluster_topology
|
|
|
|
cfg_path = Path(os.environ["PROLE_PERFSNAP_CFG"]).resolve()
|
|
snap_path = Path(os.environ["PROLE_PERFSNAP_FILE"]).resolve()
|
|
|
|
cfg = knoe_conf.load_layered_config(cfg_path, require_exists=True)
|
|
|
|
def cfg_get(section: str, key: str, default: str = "") -> str:
|
|
try:
|
|
return (cfg.get(section, key, fallback=default) or default).strip()
|
|
except Exception:
|
|
return default
|
|
|
|
mode_raw = (
|
|
cfg_get("Deployment", "MODE")
|
|
or cfg_get("Global", "DEPLOYMENT_MODE")
|
|
or cfg_get("Global", "CLUSTER_ENV")
|
|
or "k3d"
|
|
)
|
|
mode = mode_raw.lower()
|
|
project_root = cfg_path.parent.parent
|
|
knoe_data = cfg_get("Inputs", "env_setup.PROLE_DATA") or str(project_root / "data")
|
|
|
|
if mode in {"k3s", "k8s"}:
|
|
managed_kubeconfig = os.environ.get("PROLE_KUBECONFIG") or os.environ.get("KUBECONFIG")
|
|
if not managed_kubeconfig:
|
|
candidate = project_root / "knoe-k3s.kubeconfig"
|
|
if candidate.exists():
|
|
managed_kubeconfig = str(candidate)
|
|
base_cmd = _kubectl_base_cmd_for_k3s(managed_kubeconfig=managed_kubeconfig)
|
|
else:
|
|
base_cmd = ["kubectl"]
|
|
context = (
|
|
os.environ.get("KUBECTL_CONTEXT")
|
|
or cfg_get("Dev Cluster (k3d)", "KUBECTL_CONTEXT")
|
|
or ""
|
|
).strip()
|
|
if not context and mode == "k3d":
|
|
target = (
|
|
cfg_get("Global", "DEPLOYMENT_TARGET")
|
|
or cfg_get("Dev Cluster (k3d)", "DISPLAY_NAME")
|
|
or ""
|
|
).strip()
|
|
if target:
|
|
context = target if target.startswith("k3d-") else f"k3d-{target}"
|
|
if context:
|
|
base_cmd.extend(["--context", context])
|
|
output_root = Path(knoe_data) / "topology"
|
|
output_root.mkdir(parents=True, exist_ok=True)
|
|
|
|
env_map = dict(os.environ)
|
|
env_map["PROLE_CFG_FILE"] = str(cfg_path)
|
|
env_map["PROLE_DATA"] = str(Path(knoe_data).resolve())
|
|
|
|
result = discover_cluster_topology(
|
|
kubectl_base_cmd=base_cmd,
|
|
topology_root=output_root,
|
|
config=TopologyDiscoveryConfig(storage_probe_enabled=True, storage_probe_quick=True),
|
|
env=env_map,
|
|
)
|
|
|
|
payload = {
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"cfg": str(cfg_path),
|
|
"mode": mode,
|
|
"kubectl_base_cmd": base_cmd,
|
|
"topology_root": str(output_root),
|
|
"collection_status": result.topology.collection_status,
|
|
"node_count": len(result.topology.nodes),
|
|
"ready_nodes": [n.name for n in result.topology.nodes if n.ready],
|
|
"collector_applied": result.collector_applied,
|
|
"expected_ready_nodes": list(result.expected_ready_nodes),
|
|
"reported_nodes": list(result.reported_nodes),
|
|
"missing_nodes": list(result.missing_nodes),
|
|
"xml": {
|
|
"cluster": str(output_root / "cluster-topology.xml"),
|
|
"node": str(output_root / "node-topology.xml"),
|
|
},
|
|
}
|
|
|
|
snap_path.parent.mkdir(parents=True, exist_ok=True)
|
|
snap_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
print(f"Performance snapshot written: {snap_path}")
|
|
print(f"Topology root: {output_root}")
|
|
PY
|
|
;;
|
|
install)
|
|
LOGFILE="${ROOT_DIR}/logs/install-${TS}.log"
|
|
|
|
# Re-parse install-specific flags that may appear after the command word
|
|
_INSTALL_PASSTHRU=()
|
|
_skip_next=0
|
|
_next_is_config=0
|
|
for _arg in "${CMD_ARGS[@]}"; do
|
|
if [[ $_skip_next -eq 1 ]]; then
|
|
_skip_next=0
|
|
continue
|
|
fi
|
|
if [[ $_next_is_config -eq 1 ]]; then
|
|
_next_is_config=0
|
|
# Pass -c <path> through to install.sh
|
|
_INSTALL_PASSTHRU+=("-c" "$_arg")
|
|
continue
|
|
fi
|
|
case "$_arg" in
|
|
-s|--silent|-S)
|
|
SILENT=1
|
|
;;
|
|
-v|--verbose)
|
|
VERBOSE=1
|
|
export PROLE_VERBOSE=1 VERBOSE=1 CLICOLOR_FORCE=1 PY_COLORS=1
|
|
;;
|
|
-c|--config)
|
|
_next_is_config=1
|
|
;;
|
|
-r|--reset)
|
|
RESET=1
|
|
;;
|
|
*)
|
|
_INSTALL_PASSTHRU+=("$_arg")
|
|
;;
|
|
esac
|
|
done
|
|
# Rebuild INSTALL_ONLY_ARGS from (possibly updated) flags
|
|
if [[ "${SILENT}" -eq 1 ]]; then
|
|
# Ensure -S is present exactly once
|
|
_has_S=0
|
|
for _a in "${INSTALL_ONLY_ARGS[@]}"; do [[ "$_a" == "-S" ]] && _has_S=1; done
|
|
[[ $_has_S -eq 0 ]] && INSTALL_ONLY_ARGS+=("-S")
|
|
fi
|
|
if [[ "${RESET}" -eq 1 ]]; then
|
|
_has_reset=0
|
|
for _a in "${INSTALL_ONLY_ARGS[@]}"; do [[ "$_a" == "--reset" ]] && _has_reset=1; done
|
|
[[ $_has_reset -eq 0 ]] && INSTALL_ONLY_ARGS+=("--reset")
|
|
fi
|
|
if [[ "${VERBOSE}" -eq 1 ]]; then
|
|
_has_verbose=0
|
|
for _a in "${INSTALL_ONLY_ARGS[@]}"; do [[ "$_a" == "--verbose" ]] && _has_verbose=1; done
|
|
[[ $_has_verbose -eq 0 ]] && INSTALL_ONLY_ARGS+=("--verbose")
|
|
fi
|
|
CMD_ARGS=("${_INSTALL_PASSTHRU[@]}")
|
|
|
|
# Ensure OpenBao URL is set appropriately for the active mode before running install.sh.
|
|
# Honour top-level -c / --config if provided (CONFIG_PATH), falling back to default.
|
|
_cfg_for_env="${CONFIG_PATH:-${PROLE_CFG_DEFAULT}}"
|
|
for ((i=0; i<${#CMD_ARGS[@]}; i++)); do
|
|
if [[ "${CMD_ARGS[$i]}" == "-c" || "${CMD_ARGS[$i]}" == "--config" ]]; then
|
|
if [[ $((i+1)) -lt ${#CMD_ARGS[@]} ]]; then
|
|
_cfg_for_env="${CMD_ARGS[$((i+1))]}"
|
|
fi
|
|
fi
|
|
done
|
|
load_knoe_cfg "${_cfg_for_env}"
|
|
_mode_for_env=$(resolve_knoe_mode)
|
|
if [[ -z "${_mode_for_env:-}" ]]; then
|
|
_mode_for_env="k3d"
|
|
fi
|
|
ensure_openbao_url_for_mode "${_mode_for_env}"
|
|
unset _cfg_for_env _mode_for_env
|
|
|
|
if [[ "${COVERAGE}" -eq 1 ]]; then
|
|
PY_CMD=("python3" "-m" "coverage" "run" "--source=knoe" "-m" "knoe.ui.screens" "--no-gui")
|
|
else
|
|
PY_CMD=("${ROOT_DIR}/install.sh" "--no-gui")
|
|
fi
|
|
|
|
if [[ "${LOG}" -eq 1 ]]; then
|
|
run_with_log "${LOGFILE}" "${PY_CMD[@]}" "${INSTALL_ONLY_ARGS[@]}" "${CMD_ARGS[@]}"
|
|
else
|
|
"${PY_CMD[@]}" "${INSTALL_ONLY_ARGS[@]}" "${CMD_ARGS[@]}"
|
|
fi
|
|
|
|
if [[ "${COVERAGE}" -eq 1 ]]; then
|
|
echo "Generating coverage report..."
|
|
coverage report -m
|
|
coverage html
|
|
echo "HTML report generated in ${ROOT_DIR}/htmlcov/index.html"
|
|
fi
|
|
|
|
# Run status check after install completes
|
|
echo ""
|
|
echo "========================================"
|
|
echo " Post-Install Status Check"
|
|
echo "========================================"
|
|
_status_cfg="${CONFIG_PATH:-${PROLE_CFG_DEFAULT:-$(default_cfg_path)}}"
|
|
_post_status_args=()
|
|
if [[ -f "${_status_cfg}" ]]; then
|
|
_post_status_args+=("-c" "${_status_cfg}")
|
|
fi
|
|
if [[ "${VERBOSE}" -eq 1 ]]; then
|
|
_post_status_args+=("-v")
|
|
fi
|
|
if [[ -x "${ROOT_DIR}/etc/status.sh" ]]; then
|
|
"${ROOT_DIR}/etc/status.sh" "${_post_status_args[@]}" || echo "[warn] status.sh returned non-zero exit code"
|
|
else
|
|
echo "[warn] ${ROOT_DIR}/etc/status.sh not found or not executable"
|
|
fi
|
|
;;
|
|
ansible)
|
|
LOGFILE="${ROOT_DIR}/logs/ansible-${TS}.log"
|
|
prepare_ansible_color
|
|
if [[ "${LOG}" -eq 1 ]]; then
|
|
run_with_log "${LOGFILE}" "${ROOT_DIR}/ansible.sh" "${CMD_ARGS[@]}"
|
|
else
|
|
"${ROOT_DIR}/ansible.sh" "${CMD_ARGS[@]}"
|
|
fi
|
|
# SYNC AFTER ANSIBLE
|
|
if [[ -f "${ROOT_DIR}/etc/sync-knoe-cfg.py" ]]; then
|
|
python3 "${ROOT_DIR}/etc/sync-knoe-cfg.py" || true
|
|
fi
|
|
;;
|
|
site)
|
|
LOGFILE="${ROOT_DIR}/logs/site-${TS}.log"
|
|
(
|
|
set -e
|
|
prepare_ansible_color
|
|
if [[ "${RESET}" -eq 1 ]]; then
|
|
echo "Resetting k3s before site run..."
|
|
"${ROOT_DIR}/ansible.sh" -p infrastructure/playbooks/k3s_delete.yml "${CMD_ARGS[@]}"
|
|
fi
|
|
|
|
if [[ "${LOG}" -eq 1 ]]; then
|
|
run_with_log "${LOGFILE}" "${ROOT_DIR}/ansible.sh" -p infrastructure/playbooks/site.yml "${CMD_ARGS[@]}"
|
|
else
|
|
"${ROOT_DIR}/ansible.sh" -p infrastructure/playbooks/site.yml "${CMD_ARGS[@]}"
|
|
fi
|
|
# SYNC AFTER SITE
|
|
if [[ -f "${ROOT_DIR}/etc/sync-knoe-cfg.py" ]]; then
|
|
python3 "${ROOT_DIR}/etc/sync-knoe-cfg.py" || true
|
|
fi
|
|
)
|
|
;;
|
|
reset)
|
|
LOGFILE="${ROOT_DIR}/logs/reset-${TS}.log"
|
|
prepare_ansible_color
|
|
if [[ "${LOG}" -eq 1 ]]; then
|
|
run_with_log "${LOGFILE}" "${ROOT_DIR}/ansible.sh" -p infrastructure/playbooks/k3s_delete.yml "${CMD_ARGS[@]}"
|
|
else
|
|
"${ROOT_DIR}/ansible.sh" -p infrastructure/playbooks/k3s_delete.yml "${CMD_ARGS[@]}"
|
|
fi
|
|
# SYNC AFTER RESET (to clear token)
|
|
if [[ -f "${ROOT_DIR}/etc/sync-knoe-cfg.py" ]]; then
|
|
python3 "${ROOT_DIR}/etc/sync-knoe-cfg.py" || true
|
|
fi
|
|
;;
|
|
deploy)
|
|
LOGFILE="${ROOT_DIR}/logs/deploy-${TS}.log"
|
|
echo "Starting full end-to-end deployment..."
|
|
(
|
|
set -e
|
|
prepare_ansible_color
|
|
if [[ "${RESET}" -eq 1 ]]; then
|
|
echo "Step 1/3: Resetting k3s (full)..."
|
|
"${ROOT_DIR}/ansible.sh" -p infrastructure/playbooks/k3s_delete.yml "${CMD_ARGS[@]}"
|
|
else
|
|
echo "Step 1/3: Skipping k3s reset (use -r to force reset)"
|
|
fi
|
|
|
|
echo "Step 2/3: Running site deployment..."
|
|
"${ROOT_DIR}/ansible.sh" -p infrastructure/playbooks/site.yml "${CMD_ARGS[@]}"
|
|
|
|
if [[ -f "${ROOT_DIR}/etc/sync-knoe-cfg.py" ]]; then
|
|
python3 "${ROOT_DIR}/etc/sync-knoe-cfg.py" || true
|
|
fi
|
|
|
|
echo "Step 3/3: Running silent installer..."
|
|
# INSTALL_ONLY_ARGS includes -S and --reset if flags were set
|
|
"${ROOT_DIR}/install.sh" "--no-gui" "-c" "${PROLE_CFG_DEFAULT}" "${INSTALL_ONLY_ARGS[@]}" "${CMD_ARGS[@]}"
|
|
|
|
echo "Deployment completed successfully!"
|
|
) | tee "${LOGFILE}"
|
|
;;
|
|
start)
|
|
if [[ ! -f "${PROLE_CFG_DEFAULT}" ]]; then
|
|
# If no config, but we are in min mode, we can try to proceed if we just want to start containers
|
|
if [[ "${MIN_MODE}" -ne 1 ]]; then
|
|
err_msg "Missing ${PROLE_CFG_DEFAULT}"
|
|
exit 1
|
|
fi
|
|
else
|
|
load_knoe_cfg "${PROLE_CFG_DEFAULT}"
|
|
fi
|
|
|
|
local_mode=$(resolve_knoe_mode)
|
|
if [[ "${MIN_MODE}" -eq 1 ]]; then
|
|
local_mode="min"
|
|
fi
|
|
if [[ -z "${local_mode:-}" ]]; then
|
|
local_mode="k3d"
|
|
fi
|
|
|
|
if [[ "$local_mode" == "min" ]]; then
|
|
log_msg "Starting knoe-db and authority (containerd)..."
|
|
mkdir -p "${KNOE_HOME}/logs"
|
|
|
|
# Start knoe-db
|
|
log_msg "Launching knoe-db..."
|
|
nohup nerdctl run --rm --name knoe-db -p 5432:5432 knoe-db:latest > "${KNOE_HOME}/logs/knoe-db.log" 2>&1 &
|
|
|
|
# Start authority (which connects to knoedb)
|
|
log_msg "Launching authority..."
|
|
# In min mode, we assume knoe-db is available on localhost:5432
|
|
nohup nerdctl run --rm --name authority -p 8080:8080 \
|
|
-e DB_HOST=host.nerdctl.internal \
|
|
-e DB_PORT=5432 \
|
|
knoe-authority:latest > "${KNOE_HOME}/logs/authority.log" 2>&1 &
|
|
|
|
log_msg "Minimal environment started. Logs at ${KNOE_HOME}/logs"
|
|
exit 0
|
|
fi
|
|
|
|
stop_port_forwards
|
|
|
|
local_mode=$(resolve_knoe_mode)
|
|
if [[ -z "${local_mode:-}" ]]; then
|
|
local_mode="k3d"
|
|
fi
|
|
|
|
if [[ -n "${PROLE_DATA:-}" ]]; then
|
|
mkdir -p "${PROLE_DATA}" >/dev/null 2>&1 || true
|
|
if [[ -d "${PROLE_DATA}/archive" ]]; then
|
|
rm -rf "${PROLE_DATA}/archive"/*.partial >/dev/null 2>&1 || true
|
|
fi
|
|
fi
|
|
if [[ -n "${PROLE_LOGS:-}" ]]; then
|
|
mkdir -p "${PROLE_LOGS}" >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
log_msg "Starting Knoe environment (mode=${local_mode})..."
|
|
|
|
case "$local_mode" in
|
|
k3d)
|
|
cluster_name=$(resolve_k3d_cluster_name)
|
|
ensure_k3d_cluster_ready "$cluster_name"
|
|
;;
|
|
k3s|k8s)
|
|
ensure_k3s_cluster_ready
|
|
;;
|
|
*)
|
|
err_msg "Unsupported mode '${local_mode}'"
|
|
exit 2
|
|
;;
|
|
esac
|
|
|
|
log_msg "Running silent installer..."
|
|
install_args=()
|
|
if [[ "${VERBOSE}" -eq 1 ]]; then
|
|
install_args+=("-v")
|
|
fi
|
|
if [[ "${DEBUG}" -eq 1 ]]; then
|
|
install_args+=("--debug")
|
|
fi
|
|
if [[ "${RESET}" -eq 1 ]]; then
|
|
install_args+=("--reset")
|
|
fi
|
|
|
|
if [[ "${COVERAGE}" -eq 1 ]]; then
|
|
python3 -m coverage run --source=knoe -m knoe.ui.screens --no-gui -S -c "${PROLE_CFG_DEFAULT}" "${install_args[@]}"
|
|
else
|
|
"${ROOT_DIR}/install.sh" --no-gui -S -c "${PROLE_CFG_DEFAULT}" "${install_args[@]}"
|
|
fi
|
|
|
|
if [[ "${COVERAGE}" -eq 1 ]]; then
|
|
echo ""
|
|
echo "=== Coverage Report ==="
|
|
coverage report -m
|
|
coverage html
|
|
echo "HTML report generated in ${ROOT_DIR}/htmlcov/index.html"
|
|
fi
|
|
|
|
if [[ "$local_mode" == "k3d" && -x "${ROOT_DIR}/etc/init_port_forwards.sh" ]]; then
|
|
log_msg "Starting port forwards..."
|
|
"${ROOT_DIR}/etc/init_port_forwards.sh" -c "${PROLE_CFG_DEFAULT}" start
|
|
fi
|
|
;;
|
|
stop)
|
|
if [[ -f "${PROLE_CFG_DEFAULT}" ]]; then
|
|
load_knoe_cfg "${PROLE_CFG_DEFAULT}"
|
|
fi
|
|
|
|
local_mode=$(resolve_knoe_mode)
|
|
if [[ "${MIN_MODE}" -eq 1 ]]; then
|
|
local_mode="min"
|
|
fi
|
|
if [[ -z "${local_mode:-}" ]]; then
|
|
local_mode="k3d"
|
|
fi
|
|
|
|
if [[ "$local_mode" == "min" ]]; then
|
|
log_msg "Stopping containerd pods..."
|
|
nerdctl stop authority || true
|
|
nerdctl rm authority || true
|
|
nerdctl stop knoe-db || true
|
|
nerdctl rm knoe-db || true
|
|
log_msg "Minimal environment stopped."
|
|
exit 0
|
|
fi
|
|
|
|
if [[ ! -f "${PROLE_CFG_DEFAULT}" ]]; then
|
|
err_msg "Missing ${PROLE_CFG_DEFAULT}"
|
|
exit 1
|
|
fi
|
|
|
|
local_mode=$(resolve_knoe_mode)
|
|
if [[ -z "${local_mode:-}" ]]; then
|
|
local_mode="k3d"
|
|
fi
|
|
|
|
log_msg "Validating environment (mode=${local_mode})..."
|
|
case "$local_mode" in
|
|
k3d)
|
|
ensure_tools "k3d"
|
|
cluster_name=$(resolve_k3d_cluster_name)
|
|
if ! k3d cluster list "$cluster_name" >/dev/null 2>&1; then
|
|
err_msg "k3d cluster '${cluster_name}' not found"
|
|
exit 2
|
|
fi
|
|
if ! kubectl --context "${KUBECTL_CONTEXT:-k3d-${cluster_name}}" get nodes >/dev/null 2>&1; then
|
|
err_msg "k3d cluster '${cluster_name}' not reachable"
|
|
exit 2
|
|
fi
|
|
;;
|
|
k3s|k8s)
|
|
ensure_k3s_cluster_ready
|
|
;;
|
|
*)
|
|
err_msg "Unsupported mode '${local_mode}'"
|
|
exit 2
|
|
;;
|
|
esac
|
|
|
|
log_msg "Validating Garage and Barman backup configuration..."
|
|
if [[ -x "${ROOT_DIR}/etc/init_garage_store.sh" ]]; then
|
|
"${ROOT_DIR}/etc/init_garage_store.sh" start
|
|
fi
|
|
if [[ -x "${ROOT_DIR}/etc/init_cloudnative_pg.sh" ]]; then
|
|
"${ROOT_DIR}/etc/init_cloudnative_pg.sh" install-barman-plugin
|
|
fi
|
|
if [[ -x "${ROOT_DIR}/etc/init_cnpg_backup.sh" ]]; then
|
|
RUN_FIRST_BACKUP=0 "${ROOT_DIR}/etc/init_cnpg_backup.sh" start
|
|
fi
|
|
|
|
log_msg "Running full backup..."
|
|
backup_before=$(kubectl -n "${NAMESPACE:-default}" get backup \
|
|
--sort-by=.metadata.creationTimestamp \
|
|
-o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null | tail -n 1 || true)
|
|
"${ROOT_DIR}/etc/init_cnpg_backup.sh" backup
|
|
|
|
backup_name=""
|
|
for i in {1..30}; do
|
|
backup_name=$(kubectl -n "${NAMESPACE:-default}" get backup \
|
|
--sort-by=.metadata.creationTimestamp \
|
|
-o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null | tail -n 1 || true)
|
|
if [[ -n "$backup_name" && "$backup_name" != "$backup_before" ]]; then
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
if [[ -z "$backup_name" || "$backup_name" == "$backup_before" ]]; then
|
|
err_msg "Unable to detect newly created backup"
|
|
exit 1
|
|
fi
|
|
|
|
wait_for_backup "${NAMESPACE:-default}" "$backup_name" "${BACKUP_WAIT_TIMEOUT:-1800}"
|
|
|
|
archive_root="${PROLE_DATA:-${ROOT_DIR}/data}/archive"
|
|
archive_backup_from_garage "${NAMESPACE:-default}" "$backup_name" "$archive_root"
|
|
|
|
stop_monitoring_pods
|
|
|
|
if [[ -x "${ROOT_DIR}/etc/init_cloudnative_pg.sh" ]]; then
|
|
log_msg "Stopping CNPG pods..."
|
|
"${ROOT_DIR}/etc/init_cloudnative_pg.sh" stop
|
|
fi
|
|
|
|
if [[ -x "${ROOT_DIR}/etc/init_common_services.sh" ]]; then
|
|
log_msg "Stopping common services..."
|
|
if [[ "${KERBEROS_ENABLED:-}" == "1" || "${KERBEROS_ENABLED:-}" == "true" || "${KERBEROS_ENABLED:-}" == "True" ]]; then
|
|
"${ROOT_DIR}/etc/init_common_services.sh" -n "${SERVICE_NAMESPACE:-${NAMESPACE:-default}}" -k stop || true
|
|
else
|
|
"${ROOT_DIR}/etc/init_common_services.sh" -n "${SERVICE_NAMESPACE:-${NAMESPACE:-default}}" stop || true
|
|
fi
|
|
fi
|
|
;;
|
|
restart)
|
|
"${ROOT_DIR}/knoe.sh" stop "$@"
|
|
"${ROOT_DIR}/knoe.sh" start "$@"
|
|
;;
|
|
backup)
|
|
if [[ ! -f "${PROLE_CFG_DEFAULT}" ]]; then
|
|
err_msg "Missing ${PROLE_CFG_DEFAULT}"
|
|
exit 1
|
|
fi
|
|
load_knoe_cfg "${PROLE_CFG_DEFAULT}"
|
|
|
|
local_mode=$(resolve_knoe_mode)
|
|
if [[ -z "${local_mode:-}" ]]; then
|
|
local_mode="k3d"
|
|
fi
|
|
|
|
# Determine backup type: -f / --full / full → full, otherwise incremental
|
|
backup_type="incremental"
|
|
for arg in "${CMD_ARGS[@]}"; do
|
|
case "$arg" in
|
|
-f|--full|full) backup_type="full" ;;
|
|
esac
|
|
done
|
|
|
|
log_msg "Validating environment (mode=${local_mode})..."
|
|
case "$local_mode" in
|
|
k3d)
|
|
ensure_tools "k3d"
|
|
cluster_name=$(resolve_k3d_cluster_name)
|
|
if ! kubectl --context "${KUBECTL_CONTEXT:-k3d-${cluster_name}}" get nodes >/dev/null 2>&1; then
|
|
err_msg "k3d cluster '${cluster_name}' not reachable"
|
|
exit 2
|
|
fi
|
|
;;
|
|
k3s|k8s)
|
|
ensure_k3s_cluster_ready
|
|
;;
|
|
*)
|
|
err_msg "Unsupported mode '${local_mode}'"
|
|
exit 2
|
|
;;
|
|
esac
|
|
|
|
# Ensure backup infrastructure is ready
|
|
if [[ -x "${ROOT_DIR}/etc/init_garage_store.sh" ]]; then
|
|
"${ROOT_DIR}/etc/init_garage_store.sh" start
|
|
fi
|
|
if [[ -x "${ROOT_DIR}/etc/init_cloudnative_pg.sh" ]]; then
|
|
"${ROOT_DIR}/etc/init_cloudnative_pg.sh" install-barman-plugin
|
|
fi
|
|
if [[ -x "${ROOT_DIR}/etc/init_cnpg_backup.sh" ]]; then
|
|
RUN_FIRST_BACKUP=0 "${ROOT_DIR}/etc/init_cnpg_backup.sh" start
|
|
fi
|
|
|
|
log_msg "Running ${backup_type} backup..."
|
|
backup_before=$(kubectl -n "${NAMESPACE:-default}" get backup \
|
|
--sort-by=.metadata.creationTimestamp \
|
|
-o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null | tail -n 1 || true)
|
|
|
|
if [[ "$backup_type" == "full" ]]; then
|
|
"${ROOT_DIR}/etc/init_cnpg_backup.sh" backup full
|
|
else
|
|
"${ROOT_DIR}/etc/init_cnpg_backup.sh" backup incr
|
|
fi
|
|
|
|
backup_name=""
|
|
for i in {1..30}; do
|
|
backup_name=$(kubectl -n "${NAMESPACE:-default}" get backup \
|
|
--sort-by=.metadata.creationTimestamp \
|
|
-o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null | tail -n 1 || true)
|
|
if [[ -n "$backup_name" && "$backup_name" != "$backup_before" ]]; then
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
if [[ -z "$backup_name" || "$backup_name" == "$backup_before" ]]; then
|
|
err_msg "Unable to detect newly created backup"
|
|
exit 1
|
|
fi
|
|
|
|
wait_for_backup "${NAMESPACE:-default}" "$backup_name" "${BACKUP_WAIT_TIMEOUT:-1800}"
|
|
log_msg "Backup ${backup_name} (${backup_type}) completed successfully."
|
|
;;
|
|
init)
|
|
if [[ ! -f "${PROLE_CFG_DEFAULT}" ]]; then
|
|
err_msg "Missing ${PROLE_CFG_DEFAULT}"
|
|
exit 1
|
|
fi
|
|
load_knoe_cfg "${PROLE_CFG_DEFAULT}"
|
|
LOGFILE="${ROOT_DIR}/logs/init-${TS}.log"
|
|
|
|
local_mode=$(resolve_knoe_mode)
|
|
if [[ -z "${local_mode:-}" ]]; then
|
|
local_mode="k3d"
|
|
fi
|
|
|
|
run_init_scripts() {
|
|
|
|
log_msg "========================================"
|
|
log_msg " Knoe Initialization Scripts"
|
|
log_msg " Mode: ${local_mode}"
|
|
log_msg " $(date)"
|
|
log_msg "========================================"
|
|
|
|
# Resolve namespace from config
|
|
_init_ns="${NAMESPACE:-${SERVICE_NAMESPACE:-default}}"
|
|
_init_kerberos_flag=""
|
|
if [[ "${KERBEROS_ENABLED:-}" == "1" || "${KERBEROS_ENABLED:-}" == "true" || "${KERBEROS_ENABLED:-}" == "True" ]]; then
|
|
_init_kerberos_flag="-k"
|
|
fi
|
|
|
|
# Ordered list of initialization scripts matching the deployment pipeline.
|
|
# Each entry: script_name action [extra_args...]
|
|
# Scripts are run idempotently — they verify state, repair mis-deployments,
|
|
# and converge to the desired configuration.
|
|
_init_rc=0
|
|
_init_total=0
|
|
_init_pass=0
|
|
_init_fail=0
|
|
_init_skip=0
|
|
_init_failed_scripts=""
|
|
|
|
run_init_script() {
|
|
local script="$1"
|
|
shift
|
|
local script_path="${ROOT_DIR}/etc/${script}"
|
|
_init_total=$((_init_total + 1))
|
|
|
|
if [[ ! -x "$script_path" ]]; then
|
|
log_msg " [SKIP] ${script} (not found or not executable)"
|
|
_init_skip=$((_init_skip + 1))
|
|
return 0
|
|
fi
|
|
|
|
log_msg ""
|
|
log_msg "--- [${_init_total}] ${script} $* ---"
|
|
if "$script_path" "$@"; then
|
|
log_msg " [OK] ${script}"
|
|
_init_pass=$((_init_pass + 1))
|
|
else
|
|
local rc=$?
|
|
log_msg " [FAIL] ${script} (exit code ${rc})"
|
|
_init_fail=$((_init_fail + 1))
|
|
_init_rc=1
|
|
_init_failed_scripts="${_init_failed_scripts} ${script}"
|
|
fi
|
|
}
|
|
|
|
# 1. Common services (OpenBao, OpenTofu, ArgoCD, Garage)
|
|
if [[ -n "$_init_kerberos_flag" ]]; then
|
|
run_init_script init_common_services.sh -n "$_init_ns" $_init_kerberos_flag start
|
|
else
|
|
run_init_script init_common_services.sh -n "$_init_ns" start
|
|
fi
|
|
|
|
# 2. knoe-auth (cluster-internal KDC + SSO gateway) — must be ready before CNPG
|
|
if [[ -n "$_init_kerberos_flag" ]]; then
|
|
run_init_script init_knoe_auth.sh initialize
|
|
fi
|
|
|
|
# 3. CloudNative-PG operator and database cluster
|
|
run_init_script init_cloudnative_pg.sh initialize
|
|
|
|
# 4. Kerberos (if enabled)
|
|
if [[ -n "$_init_kerberos_flag" ]]; then
|
|
run_init_script init_kerberos.sh initialize
|
|
fi
|
|
|
|
# 5. Database backup infrastructure
|
|
run_init_script init_cnpg_backup.sh start
|
|
|
|
# 6. Kong API Gateway
|
|
run_init_script init_kong.sh -n "${NAMESPACE:-knoe-db}" start
|
|
|
|
# 7. Monitoring (Grafana, Prometheus)
|
|
run_init_script init_monitoring.sh initialize
|
|
|
|
# 8. Nginx Ingress (non-k3d only)
|
|
if [[ "$local_mode" != "k3d" ]]; then
|
|
run_init_script init_nginx_ingress.sh initialize
|
|
fi
|
|
|
|
# 9. Port forwards (k3d only)
|
|
if [[ "$local_mode" == "k3d" && -x "${ROOT_DIR}/etc/init_port_forwards.sh" ]]; then
|
|
run_init_script init_port_forwards.sh -c "${PROLE_CFG_DEFAULT}" start
|
|
fi
|
|
|
|
# Summary
|
|
log_msg ""
|
|
log_msg "========================================"
|
|
log_msg " Initialization Summary"
|
|
log_msg "========================================"
|
|
log_msg " Total: ${_init_total}"
|
|
log_msg " Passed: ${_init_pass}"
|
|
log_msg " Failed: ${_init_fail}"
|
|
log_msg " Skipped: ${_init_skip}"
|
|
if [[ $_init_fail -gt 0 ]]; then
|
|
log_msg ""
|
|
log_msg " Failed scripts:${_init_failed_scripts}"
|
|
fi
|
|
log_msg "========================================"
|
|
|
|
# Run status check
|
|
if [[ -x "${ROOT_DIR}/etc/status.sh" ]]; then
|
|
log_msg ""
|
|
log_msg " Post-Init Status Check"
|
|
log_msg "========================================"
|
|
_status_args=()
|
|
if [[ -f "${PROLE_CFG_DEFAULT}" ]]; then
|
|
_status_args+=("-c" "${PROLE_CFG_DEFAULT}")
|
|
fi
|
|
if [[ "${VERBOSE}" -eq 1 ]]; then
|
|
_status_args+=("-v")
|
|
fi
|
|
"${ROOT_DIR}/etc/status.sh" "${_status_args[@]}" || true
|
|
fi
|
|
|
|
return "$_init_rc"
|
|
}
|
|
|
|
if [[ "${LOG}" -eq 1 ]]; then
|
|
mkdir -p "$(dirname "${LOGFILE}")"
|
|
log_msg "Logging to ${LOGFILE}"
|
|
run_init_scripts 2>&1 | tee "${LOGFILE}"
|
|
exit "${PIPESTATUS[0]}"
|
|
else
|
|
run_init_scripts
|
|
exit $?
|
|
fi
|
|
;;
|
|
passwd)
|
|
if [[ -x "${ROOT_DIR}/etc/knoe-db-passwwd.sh" ]]; then
|
|
"${ROOT_DIR}/etc/knoe-db-passwwd.sh" "${CMD_ARGS[@]}"
|
|
else
|
|
err_msg "Missing ${ROOT_DIR}/etc/knoe-db-passwwd.sh"
|
|
exit 1
|
|
fi
|
|
;;
|
|
status)
|
|
status_args=()
|
|
_status_cfg="${CONFIG_PATH:-${PROLE_CFG_DEFAULT:-$(default_cfg_path)}}"
|
|
if [[ -f "${_status_cfg}" ]]; then
|
|
status_args+=("-c" "${_status_cfg}")
|
|
fi
|
|
if [[ "${VERBOSE}" -eq 1 ]]; then
|
|
status_args+=("-v")
|
|
fi
|
|
# Pass any remaining command-level args (e.g. extra flags)
|
|
status_args+=("${CMD_ARGS[@]}")
|
|
if [[ -x "${ROOT_DIR}/etc/status.sh" ]]; then
|
|
"${ROOT_DIR}/etc/status.sh" "${status_args[@]}"
|
|
else
|
|
err_msg "Missing ${ROOT_DIR}/etc/status.sh"
|
|
exit 1
|
|
fi
|
|
;;
|
|
*)
|
|
# Default to ansible if command not matched
|
|
LOGFILE="${ROOT_DIR}/logs/${CMD}-${TS}.log"
|
|
if [[ "${LOG}" -eq 1 ]]; then
|
|
run_with_log "${LOGFILE}" "${ROOT_DIR}/ansible.sh" "${CMD}" "${CMD_ARGS[@]}"
|
|
else
|
|
"${ROOT_DIR}/ansible.sh" "${CMD}" "${CMD_ARGS[@]}"
|
|
fi
|
|
# SYNC AFTER ANSIBLE
|
|
if [[ -f "${ROOT_DIR}/etc/sync-knoe-cfg.py" ]]; then
|
|
python3 "${ROOT_DIR}/etc/sync-knoe-cfg.py" || true
|
|
fi
|
|
;;
|
|
esac
|