mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 12:03:59 +00:00
301 lines
9.2 KiB
Bash
Executable File
301 lines
9.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
# init_opentofu.sh
|
|
# Purpose:
|
|
# - Deploy OpenTofu control plane into Kubernetes
|
|
# - Configure admin access using the Knoe DB root password
|
|
|
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
|
|
# Shared option parsing for common core scripts
|
|
# shellcheck disable=SC1090
|
|
source "$SCRIPT_DIR/common_core_lib.sh"
|
|
|
|
common_core_preparse_config "$@"
|
|
|
|
# shellcheck disable=SC1090
|
|
source "$SCRIPT_DIR/knoe_cfg.sh"
|
|
|
|
set -- "${COMMON_CORE_ARGS[@]}"
|
|
common_core_parse_args "$@"
|
|
|
|
if [[ "${COMMON_CORE_HELP:-0}" == 1 ]]; then
|
|
common_core_usage "$0"
|
|
exit 0
|
|
fi
|
|
|
|
if [[ -n "${COMMON_CORE_PARSE_ERROR:-}" ]]; then
|
|
echo "ERROR: ${COMMON_CORE_PARSE_ERROR}" >&2
|
|
common_core_usage "$0"
|
|
exit 2
|
|
fi
|
|
|
|
ACTION="$COMMON_CORE_ACTION"
|
|
NAMESPACE="$(common_core_resolve_namespace "default")"
|
|
common_core_apply_namespace "$NAMESPACE"
|
|
|
|
if [[ -z "${KNOE_SERVICE:-}" ]]; then
|
|
echo "ERROR: KNOE_SERVICE is not defined. Provide KNOE_HOME/env.sh or ~/.knoe/env.sh" >&2
|
|
exit 1
|
|
fi
|
|
|
|
OPENTOFU_NAME=${OPENTOFU_NAME:-opentofu}
|
|
OPENTOFU_ADMIN_USER=${OPENTOFU_ADMIN_USER:-admin}
|
|
OPENTOFU_ADMIN_PASSWORD_SOURCE=${OPENTOFU_ADMIN_PASSWORD_SOURCE:-}
|
|
|
|
if [[ -d "$SCRIPT_DIR/../k8s/opentofu" ]]; then
|
|
OPENTOFU_MANIFEST_DIR="$SCRIPT_DIR/../k8s/opentofu"
|
|
elif [[ -n "${KNOE_HOME:-}" && -d "$KNOE_HOME/k8s/opentofu" ]]; then
|
|
OPENTOFU_MANIFEST_DIR="$KNOE_HOME/k8s/opentofu"
|
|
else
|
|
OPENTOFU_MANIFEST_DIR="$SCRIPT_DIR/../k8s/opentofu"
|
|
fi
|
|
|
|
OPENTOFU_NAMESPACE=${OPENTOFU_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-default}}}
|
|
OPENTOFU_SECRET_NAMESPACE=${OPENTOFU_SECRET_NAMESPACE:-${NAMESPACE:-default}}
|
|
OPENTOFU_OPENBAO_NAMESPACE=${OPENTOFU_OPENBAO_NAMESPACE:-${SERVICE_NAMESPACE:-${OPENTOFU_NAMESPACE}}}
|
|
|
|
ensure_tools() {
|
|
for t in kubectl openssl curl jq; do
|
|
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
|
|
done
|
|
}
|
|
|
|
ensure_namespace() {
|
|
if ! kubectl get namespace "$OPENTOFU_NAMESPACE" >/dev/null 2>&1; then
|
|
echo "Creating namespace '$OPENTOFU_NAMESPACE' ..."
|
|
kubectl create namespace "$OPENTOFU_NAMESPACE" >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
|
|
openbao_url() {
|
|
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
|
|
echo "$PROLE_OPENBAO_URL"
|
|
return 0
|
|
fi
|
|
if knoe_is_in_cluster; then
|
|
echo "http://openbao.${OPENTOFU_OPENBAO_NAMESPACE}.svc.cluster.local:8200"
|
|
return 0
|
|
fi
|
|
|
|
# In k3s mode, scripts run outside the cluster must reach OpenBao via the k3s host
|
|
# (never via localhost or kubectl port-forward).
|
|
if [[ "${KNOE_MODE:-${DEPLOYMENT_MODE:-}}" == "k3s" ]]; then
|
|
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
|
|
echo "http://${host}:8200"
|
|
return 0
|
|
fi
|
|
fi
|
|
echo ""
|
|
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
|
|
echo ""
|
|
return 0
|
|
}
|
|
|
|
openbao_token() {
|
|
if [[ -f "$KNOE_SERVICE/secrets/openbao-root-token" ]]; then
|
|
cat "$KNOE_SERVICE/secrets/openbao-root-token"
|
|
else
|
|
echo "${OPENBAO_ROOT_TOKEN:-}"
|
|
fi
|
|
}
|
|
|
|
fetch_openbao_secret() {
|
|
local path="$1"
|
|
local key="$2"
|
|
local token url
|
|
token=$(openbao_token)
|
|
url=$(openbao_url)
|
|
if [[ -z "$token" || -z "$url" ]]; then
|
|
echo ""
|
|
return 0
|
|
fi
|
|
curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/$path" | jq -r ".data.data.\"$key\"" || echo ""
|
|
}
|
|
|
|
is_anchor() {
|
|
case "${1:-}" in
|
|
'${OPENBAO:'*|'${KNOE_SECRET:'*) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
resolve_admin_password() {
|
|
OPENTOFU_ADMIN_PASSWORD_SOURCE=""
|
|
|
|
if [[ -n "${OPENTOFU_ADMIN_PASSWORD:-}" ]] && ! is_anchor "${OPENTOFU_ADMIN_PASSWORD:-}"; then
|
|
OPENTOFU_ADMIN_PASSWORD_SOURCE="env"
|
|
return 0
|
|
fi
|
|
|
|
if [[ -z "${OPENTOFU_ADMIN_PASSWORD:-}" ]]; then
|
|
if [[ -n "${DB_PASSWORD:-}" ]] && ! is_anchor "${DB_PASSWORD:-}"; then
|
|
OPENTOFU_ADMIN_PASSWORD="${DB_PASSWORD}"
|
|
OPENTOFU_ADMIN_PASSWORD_SOURCE="db_password"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "${OPENTOFU_ADMIN_PASSWORD:-}" ]] || is_anchor "${OPENTOFU_ADMIN_PASSWORD:-}"; then
|
|
local fetched
|
|
fetched=$(fetch_openbao_secret "knoe/${OPENTOFU_SECRET_NAMESPACE:-default}/db" "password")
|
|
if [[ -n "$fetched" && "$fetched" != "null" ]]; then
|
|
OPENTOFU_ADMIN_PASSWORD="$fetched"
|
|
OPENTOFU_ADMIN_PASSWORD_SOURCE="openbao"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Fallback: read password from existing Kubernetes secret
|
|
if [[ -z "${OPENTOFU_ADMIN_PASSWORD:-}" ]]; then
|
|
local k8s_pw
|
|
k8s_pw=$(kubectl get secret opentofu-admin -n "$OPENTOFU_NAMESPACE" \
|
|
-o jsonpath='{.data.admin_password}' 2>/dev/null | base64 -d 2>/dev/null || true)
|
|
if [[ -n "$k8s_pw" ]]; then
|
|
OPENTOFU_ADMIN_PASSWORD="$k8s_pw"
|
|
OPENTOFU_ADMIN_PASSWORD_SOURCE="k8s_secret"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Last resort: generate a temporary password (will be overwritten later when user input becomes available).
|
|
if [[ -z "${OPENTOFU_ADMIN_PASSWORD:-}" ]]; then
|
|
if declare -F knoe_generate_password >/dev/null 2>&1; then
|
|
OPENTOFU_ADMIN_PASSWORD="$(knoe_generate_password 32)"
|
|
else
|
|
OPENTOFU_ADMIN_PASSWORD="$(openssl rand -base64 24 2>/dev/null | tr -d '\n' | tr '+/' '-_' | tr -d '=' | head -c 32)"
|
|
fi
|
|
OPENTOFU_ADMIN_PASSWORD_SOURCE="generated"
|
|
if [[ -z "${OPENTOFU_ADMIN_PASSWORD:-}" ]]; then
|
|
return 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
get_existing_admin_password() {
|
|
kubectl get secret opentofu-admin -n "$OPENTOFU_NAMESPACE" \
|
|
-o jsonpath='{.data.admin_password}' 2>/dev/null | base64 -d 2>/dev/null || true
|
|
}
|
|
|
|
opentofu_secret_exists() {
|
|
kubectl get secret opentofu-admin -n "$OPENTOFU_NAMESPACE" >/dev/null 2>&1
|
|
}
|
|
|
|
prompt_admin_password() {
|
|
# Only prompt when interactive.
|
|
if [[ ! -t 0 ]]; then
|
|
# Non-interactive fallback: generate a temporary password.
|
|
if resolve_admin_password; then
|
|
return 0
|
|
fi
|
|
echo "ERROR: OpenTofu admin password is required but no prompt is available (non-interactive)." >&2
|
|
echo "Set OPENTOFU_ADMIN_PASSWORD (or DB_PASSWORD) or create the 'opentofu-admin' secret in namespace '$OPENTOFU_NAMESPACE'." >&2
|
|
return 2
|
|
fi
|
|
|
|
echo "OpenTofu admin password is required to bootstrap the 'opentofu-admin' secret." >&2
|
|
echo "It will default to the DB/root master password when available." >&2
|
|
|
|
local p1 p2
|
|
while true; do
|
|
read -r -s -p "Enter OpenTofu Admin Password: " p1
|
|
echo >&2
|
|
if [[ -z "${p1:-}" ]]; then
|
|
echo "Password cannot be empty." >&2
|
|
continue
|
|
fi
|
|
read -r -s -p "Confirm OpenTofu Admin Password: " p2
|
|
echo >&2
|
|
if [[ "$p1" != "$p2" ]]; then
|
|
echo "Passwords do not match. Please try again." >&2
|
|
continue
|
|
fi
|
|
OPENTOFU_ADMIN_PASSWORD="$p1"
|
|
return 0
|
|
done
|
|
}
|
|
|
|
ensure_opentofu_secret() {
|
|
local tmp
|
|
tmp=$(mktemp)
|
|
local hash
|
|
hash=$(printf "%s" "$OPENTOFU_ADMIN_PASSWORD" | openssl passwd -apr1 -stdin)
|
|
printf "%s:%s\n" "$OPENTOFU_ADMIN_USER" "$hash" >"$tmp"
|
|
|
|
kubectl create secret generic opentofu-admin \
|
|
-n "$OPENTOFU_NAMESPACE" \
|
|
--from-literal=admin_password="$OPENTOFU_ADMIN_PASSWORD" \
|
|
--from-file=auth="$tmp" \
|
|
--dry-run=client -o yaml | kubectl apply -f - >/dev/null
|
|
|
|
rm -f "$tmp"
|
|
}
|
|
|
|
apply_k8s() {
|
|
echo "Applying OpenTofu manifest to namespace '$OPENTOFU_NAMESPACE' ..."
|
|
knoe_render_manifest "$OPENTOFU_MANIFEST_DIR/deployment.yaml" | kubectl apply -n "$OPENTOFU_NAMESPACE" -f -
|
|
kubectl rollout status deploy/$OPENTOFU_NAME -n "$OPENTOFU_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} || true
|
|
}
|
|
|
|
delete_k8s() {
|
|
echo "Removing OpenTofu resources from namespace '$OPENTOFU_NAMESPACE' ..."
|
|
kubectl delete -n "$OPENTOFU_NAMESPACE" -f "$OPENTOFU_MANIFEST_DIR/deployment.yaml" --ignore-not-found
|
|
kubectl delete -n "$OPENTOFU_NAMESPACE" secret opentofu-admin --ignore-not-found || true
|
|
}
|
|
|
|
status_k8s() {
|
|
kubectl -n "$OPENTOFU_NAMESPACE" get deploy "$OPENTOFU_NAME" 2>/dev/null || true
|
|
kubectl -n "$OPENTOFU_NAMESPACE" get svc "$OPENTOFU_NAME" 2>/dev/null || true
|
|
}
|
|
|
|
case "${ACTION:-}" in
|
|
start|initialize|update|reload|restart)
|
|
ensure_tools
|
|
ensure_namespace
|
|
# Always resolve a password (DB_PASSWORD/OpenBao/env) so we can rotate later if needed.
|
|
if ! resolve_admin_password; then
|
|
prompt_admin_password
|
|
fi
|
|
|
|
if opentofu_secret_exists; then
|
|
existing_pw="$(get_existing_admin_password)"
|
|
if [[ "${OPENTOFU_ADMIN_PASSWORD_SOURCE:-}" != "k8s_secret" && -n "${OPENTOFU_ADMIN_PASSWORD:-}" && "${OPENTOFU_ADMIN_PASSWORD}" != "${existing_pw:-}" ]]; then
|
|
echo "[INFO] Updating OpenTofu admin secret in namespace '$OPENTOFU_NAMESPACE' (source: ${OPENTOFU_ADMIN_PASSWORD_SOURCE})."
|
|
ensure_opentofu_secret
|
|
else
|
|
echo "[OK] OpenTofu admin secret already exists in namespace '$OPENTOFU_NAMESPACE'; leaving unchanged."
|
|
fi
|
|
else
|
|
if [[ "${OPENTOFU_ADMIN_PASSWORD_SOURCE:-}" == "generated" ]]; then
|
|
echo "[WARN] No OpenTofu admin password available yet; generating a temporary password for bootstrap."
|
|
fi
|
|
ensure_opentofu_secret
|
|
fi
|
|
apply_k8s
|
|
knoe_register_port_forward "opentofu" "${OPENTOFU_NAMESPACE:-default}" "svc/opentofu" "8080" "8080" "0.0.0.0" "TCP" "OpenTofu"
|
|
;;
|
|
stop)
|
|
ensure_tools
|
|
delete_k8s
|
|
;;
|
|
status)
|
|
ensure_tools
|
|
status_k8s
|
|
;;
|
|
*)
|
|
common_core_usage "$0"
|
|
exit 1
|
|
;;
|
|
esac
|