#!/usr/bin/env bash set -euo pipefail # init_opentofu.sh # Purpose: # - Deploy OpenTofu control plane into Kubernetes # - Configure admin access using the Prole 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/prole_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 "${PROLE_SERVICE:-}" ]]; then echo "ERROR: PROLE_SERVICE is not defined. Provide PROLE_HOME/env.sh or ~/.prole/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 "${PROLE_HOME:-}" && -d "$PROLE_HOME/k8s/opentofu" ]]; then OPENTOFU_MANIFEST_DIR="$PROLE_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 prole_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 [[ "${PROLE_MODE:-${DEPLOYMENT_MODE:-}}" == "k3s" ]]; then if command -v _prole_host_from_url >/dev/null 2>&1; then local host host=$(_prole_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 "$PROLE_SERVICE/secrets/openbao-root-token" ]]; then cat "$PROLE_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:'*|'${PROLE_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 "prole/${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 prole_generate_password >/dev/null 2>&1; then OPENTOFU_ADMIN_PASSWORD="$(prole_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' ..." prole_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 prole_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