feat: infrastructure and installer updates for k3s, OpenTofu, and prole-db

- Add k3s start/stop Ansible playbooks and roles.

- Implement OpenTofu initialization scripts and k8s manifests.

- Update ncurses installer with OpenTofu support and improved k3s integration.

- Add mode support (--mode) to etc/ initialization scripts.

- Update prole-db with recovery, barman objectstore, and SSH OpenBao support.

- Refine k8s manifests for OpenBao and prole-db.
This commit is contained in:
chrisfu 2026-02-05 21:27:18 -08:00
parent 82dc879116
commit da2f6600ba
40 changed files with 1830 additions and 149 deletions

View File

@ -0,0 +1,22 @@
# OpenTofu k3s Pipeline
This pipeline re-deploys the Prole environment into a k3s cluster using OpenTofu.
## Usage
1) Ensure `opentofu.auto.tfvars` is populated (install.py will generate it).
2) Sync manifests into `deploy/opentofu/k3s/manifests`.
3) Run:
```bash
tofu init
tofu plan
tofu apply
```
## Files
- `main.tf`: Applies Kubernetes manifests with the configured namespace.
- `variables.tf`: Pipeline inputs (server URL, token, namespace).
- `opentofu.auto.tfvars`: Auto-generated values from Prole install/config.
- `manifests/`: Copy of `k8s/` manifests to re-deploy.

View File

@ -0,0 +1,72 @@
terraform {
required_version = ">= 1.6.0"
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.30"
}
}
}
provider "kubernetes" {
host = var.k3s_server_url
token = var.k3s_token
insecure = true
}
locals {
manifest_dir = "${path.module}/manifests"
manifest_files = fileset(local.manifest_dir, "**/*.yaml")
raw_documents = flatten([
for f in local.manifest_files : [
for doc in split("\n---", trimspace(file("${local.manifest_dir}/${f}"))) :
trimspace(doc)
if trimspace(doc) != ""
]
])
decoded_documents = [
for doc in local.raw_documents : yamldecode(doc)
if try(yamldecode(doc).kind, "") != ""
]
cluster_scoped_kinds = toset([
"Namespace",
"CustomResourceDefinition",
"ClusterRole",
"ClusterRoleBinding",
"ClusterIssuer",
"PersistentVolume",
"StorageClass",
"MutatingWebhookConfiguration",
"ValidatingWebhookConfiguration",
])
namespaced_documents = [
for m in local.decoded_documents :
contains(local.cluster_scoped_kinds, m.kind) ? m : merge(
m,
{
metadata = merge(
lookup(m, "metadata", {}),
{ namespace = var.namespace }
)
}
)
]
}
resource "kubernetes_manifest" "namespace" {
manifest = {
apiVersion = "v1"
kind = "Namespace"
metadata = { name = var.namespace }
}
}
resource "kubernetes_manifest" "resources" {
for_each = { for idx, m in local.namespaced_documents : tostring(idx) => m }
manifest = each.value
depends_on = [kubernetes_manifest.namespace]
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,16 @@
variable "k3s_server_url" {
type = string
description = "K3s API server URL (e.g., https://pi.prole.org:6443)"
}
variable "k3s_token" {
type = string
description = "K3s API token"
sensitive = true
}
variable "namespace" {
type = string
description = "Target namespace for Prole resources"
default = "default"
}

View File

@ -36,7 +36,7 @@ export_docker_images() {
# Collect images from K8s manifests
# Robust extraction of image names from YAML files
find "${PROJECT_ROOT}/k8s/prole" "${PROJECT_ROOT}/k8s/openbao" -name "*.yaml" -exec grep -h "image:" {} + | awk -F'image:' '{print $2}' | awk '{print $1}' | sed "s/['\"]//g" > /tmp/prole_images.txt
find "${PROJECT_ROOT}/k8s/prole" "${PROJECT_ROOT}/k8s/openbao" "${PROJECT_ROOT}/k8s/opentofu" -name "*.yaml" -exec grep -h "image:" {} + | awk -F'image:' '{print $2}' | awk '{print $1}' | sed "s/['\"]//g" > /tmp/prole_images.txt
# Also check Supabase compose if it exists
if [[ -f "${PROJECT_ROOT}/supabase/docker/docker-compose.yml" ]]; then

View File

@ -11,6 +11,14 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh"
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
prole_set_mode "${2:-}"
shift 2
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
prole_set_mode "${1#*=}"
shift
fi
PROLE_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
VAULT_PASS_FILE="$PROLE_ROOT/.vault_pass"

View File

@ -16,6 +16,14 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh"
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
prole_set_mode "${2:-}"
shift 2
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
prole_set_mode "${1#*=}"
shift
fi
ACTION=${1:-start}
NAMESPACE_AUTH="authority"
KDC_NAME="dog"

View File

@ -25,6 +25,14 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh"
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
prole_set_mode "${2:-}"
shift 2
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
prole_set_mode "${1#*=}"
shift
fi
if [[ -z "${PROLE_SERVICE:-}" ]]; then
echo "ERROR: PROLE_SERVICE is not defined. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2
exit 1
@ -38,15 +46,26 @@ DOMAIN=${DOMAIN:-prole.org}
KRB5_KDC=${KRB5_KDC:-}
KRB5_ADMIN=${KRB5_ADMIN:-}
CNPG_MANIFEST_OVERRIDE=${CNPG_MANIFEST_OVERRIDE:-}
PROLE_HOME=${PROLE_HOME:-$(cd "$SCRIPT_DIR/.." && pwd)}
BACKUP_DIR=${BACKUP_DIR:-$PROLE_HOME/prole/backup}
BACKUP_WAIT_TIMEOUT=${BACKUP_WAIT_TIMEOUT:-1800}
RECOVERY_TEMPLATE="$SCRIPT_DIR/../k8s/prole/prole-db-recovery.yaml.tpl"
CNPG_FORCE_ROLLOUT_SCRIPT="$SCRIPT_DIR/init_prole-db.sh"
BARMAN_PLUGIN_MANIFEST_URL=${BARMAN_PLUGIN_MANIFEST_URL:-}
BARMAN_PLUGIN_FALLBACK_VERSION=${BARMAN_PLUGIN_FALLBACK_VERSION:-0.9.0}
CERT_MANAGER_MANIFEST_URL=${CERT_MANAGER_MANIFEST_URL:-}
CERT_MANAGER_FALLBACK_VERSION=${CERT_MANAGER_FALLBACK_VERSION:-1.19.3}
# Support both PROLE_HOME/k8s and sibling k8s directory
if [[ -d "$SCRIPT_DIR/../k8s/prole" ]]; then
CNPG_MANIFEST="$SCRIPT_DIR/../k8s/prole/prole-db.yaml"
K8S_PROLE_DIR="$SCRIPT_DIR/../k8s/prole"
elif [[ -n "${PROLE_HOME:-}" && -d "$PROLE_HOME/k8s/prole" ]]; then
CNPG_MANIFEST="$PROLE_HOME/k8s/prole/prole-db.yaml"
K8S_PROLE_DIR="$PROLE_HOME/k8s/prole"
else
CNPG_MANIFEST="$SCRIPT_DIR/../k8s/prole/prole-db.yaml"
K8S_PROLE_DIR="$SCRIPT_DIR/../k8s/prole"
fi
CNPG_MANIFEST="$K8S_PROLE_DIR/prole-db.yaml"
BARMAN_OBJECTSTORE_MANIFEST="$K8S_PROLE_DIR/prole-db-barman-objectstore.yaml"
SECRETS_DIR="$PROLE_SERVICE/secrets"
# Resolving CNPG admin keys.
@ -146,6 +165,8 @@ ensure_grafana_admin_secret() {
ensure_cnpg_operator() {
if kubectl get deployment -n cnpg-system cnpg-controller-manager >/dev/null 2>&1; then
kubectl -n cnpg-system rollout status deploy/cnpg-controller-manager --timeout=180s || true
wait_for_cnpg_webhook 180 || true
return 0
fi
@ -158,7 +179,154 @@ ensure_cnpg_operator() {
kubectl apply --server-side -f "$yaml_url"
if kubectl get deployment -n cnpg-system cnpg-controller-manager >/dev/null 2>&1; then
kubectl -n cnpg-system rollout status deploy/cnpg-controller-manager --timeout=120s || true
kubectl -n cnpg-system rollout status deploy/cnpg-controller-manager --timeout=180s || true
wait_for_cnpg_webhook 180 || true
fi
}
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 "")
if [[ -z "$tag" || "$tag" == "null" ]]; then
echo "v${BARMAN_PLUGIN_FALLBACK_VERSION}"
return 0
fi
echo "$tag"
}
resolve_barman_plugin_manifest_url() {
if [[ -n "$BARMAN_PLUGIN_MANIFEST_URL" ]]; then
echo "$BARMAN_PLUGIN_MANIFEST_URL"
return 0
fi
local tag
tag=$(get_latest_barman_plugin_version)
echo "https://github.com/cloudnative-pg/plugin-barman-cloud/releases/download/${tag}/manifest.yaml"
}
cert_manager_ready() {
kubectl get crd certificates.cert-manager.io >/dev/null 2>&1
}
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 "")
if [[ -z "$tag" || "$tag" == "null" ]]; then
echo "v${CERT_MANAGER_FALLBACK_VERSION}"
return 0
fi
echo "$tag"
}
resolve_cert_manager_manifest_url() {
if [[ -n "$CERT_MANAGER_MANIFEST_URL" ]]; then
echo "$CERT_MANAGER_MANIFEST_URL"
return 0
fi
local tag
tag=$(get_latest_cert_manager_version)
echo "https://github.com/cert-manager/cert-manager/releases/download/${tag}/cert-manager.yaml"
}
ensure_cert_manager() {
if cert_manager_ready; then
if kubectl -n cert-manager get deploy cert-manager >/dev/null 2>&1; then
kubectl -n cert-manager rollout status deploy/cert-manager --timeout=180s || true
kubectl -n cert-manager rollout status deploy/cert-manager-webhook --timeout=180s || true
kubectl -n cert-manager rollout status deploy/cert-manager-cainjector --timeout=180s || true
fi
return 0
fi
local cm_url
cm_url=$(resolve_cert_manager_manifest_url)
echo "Installing cert-manager from ${cm_url} ..."
kubectl apply -f "$cm_url"
if kubectl -n cert-manager get deploy cert-manager >/dev/null 2>&1; then
kubectl -n cert-manager rollout status deploy/cert-manager --timeout=180s || true
kubectl -n cert-manager rollout status deploy/cert-manager-webhook --timeout=180s || true
kubectl -n cert-manager rollout status deploy/cert-manager-cainjector --timeout=180s || true
fi
}
wait_for_barman_crd() {
local timeout=${1:-120}
local start_time
start_time=$(date +%s)
while true; do
if kubectl get crd objectstores.barmancloud.cnpg.io >/dev/null 2>&1; then
return 0
fi
if (( $(date +%s) - start_time > timeout )); then
return 1
fi
sleep 5
done
}
ensure_barman_plugin() {
local crd_present deploy_present
crd_present=false
deploy_present=false
if kubectl get crd objectstores.barmancloud.cnpg.io >/dev/null 2>&1; then
crd_present=true
fi
if kubectl -n cnpg-system get deploy barman-cloud >/dev/null 2>&1; then
deploy_present=true
fi
if [[ "$crd_present" == "true" && "$deploy_present" == "true" ]]; then
kubectl -n cnpg-system rollout status deploy/barman-cloud --timeout=180s || true
return 0
fi
ensure_cert_manager
local plugin_url
plugin_url=$(resolve_barman_plugin_manifest_url)
echo "Installing Barman Cloud plugin from ${plugin_url} ..."
kubectl apply -f "$plugin_url"
if ! wait_for_barman_crd 120; then
echo "WARN: Barman Cloud ObjectStore CRD not ready after install." >&2
fi
if kubectl -n cnpg-system get deploy barman-cloud >/dev/null 2>&1; then
kubectl -n cnpg-system rollout status deploy/barman-cloud --timeout=180s || true
fi
}
wait_for_cnpg_webhook() {
local timeout=${1:-120}
local start_time
start_time=$(date +%s)
echo "Waiting for CNPG webhook service endpoints to be ready..."
while true; do
local endpoints
endpoints=$(kubectl -n cnpg-system get endpoints cnpg-webhook-service -o jsonpath='{.subsets[*].addresses[*].ip}' 2>/dev/null || true)
if [[ -n "$endpoints" ]]; then
echo "CNPG webhook service has endpoints."
return 0
fi
if (( $(date +%s) - start_time > timeout )); then
echo "WARN: CNPG webhook endpoints not ready after ${timeout}s."
return 1
fi
sleep 5
done
}
apply_barman_objectstore_if_present() {
if [[ ! -f "$BARMAN_OBJECTSTORE_MANIFEST" ]]; then
return 0
fi
if kubectl get crd objectstores.barmancloud.cnpg.io >/dev/null 2>&1; then
kubectl apply -n "$NAMESPACE" -f "$BARMAN_OBJECTSTORE_MANIFEST"
else
echo "WARN: Barman Cloud ObjectStore CRD not found; skipping $BARMAN_OBJECTSTORE_MANIFEST."
fi
}
@ -171,27 +339,30 @@ ensure_prole_stack_resources() {
return 1
fi
local dir file
dir="$SCRIPT_DIR/../k8s/prole"
dir="$K8S_PROLE_DIR"
for file in "$dir"/*.yaml; do
case "$(basename "$file")" in
prole-db.yaml|kustomization.yaml|supabase-*.yaml)
prole-db.yaml|kustomization.yaml|supabase-*.yaml|prole-db-barman-objectstore.yaml)
continue
;;
esac
kubectl apply -n "$NAMESPACE" -f "$file"
done
kubectl apply -n "$NAMESPACE" -f "$CNPG_MANIFEST_OVERRIDE"
apply_barman_objectstore_if_present
apply_cnpg_cluster_manifest "$CNPG_MANIFEST_OVERRIDE"
else
local dir file
dir="$SCRIPT_DIR/../k8s/prole"
dir="$K8S_PROLE_DIR"
for file in "$dir"/*.yaml; do
case "$(basename "$file")" in
kustomization.yaml|supabase-*.yaml)
prole-db.yaml|kustomization.yaml|supabase-*.yaml|prole-db-barman-objectstore.yaml)
continue
;;
esac
kubectl apply -n "$NAMESPACE" -f "$file"
done
apply_barman_objectstore_if_present
apply_cnpg_cluster_manifest "$CNPG_MANIFEST"
fi
# Ensure prole-index-html exists for prole deployment readiness probe
@ -213,6 +384,30 @@ ensure_prole_stack_resources() {
fi
}
apply_cnpg_cluster_manifest() {
local manifest="$1"
local attempts=${CNPG_APPLY_RETRIES:-6}
local i out
for ((i=1; i<=attempts; i++)); do
if out=$(kubectl apply -n "$NAMESPACE" -f "$manifest" 2>&1); then
printf '%s\n' "$out"
return 0
fi
if echo "$out" | grep -q "cnpg-webhook-service"; then
echo "CNPG webhook not ready yet (attempt $i/$attempts). Retrying..."
sleep 5
continue
fi
echo "$out" >&2
return 1
done
echo "ERROR: Failed to apply CNPG manifest after $attempts attempts." >&2
echo "$out" >&2
return 1
}
wait_for_cnpg_pods() {
local timeout=${1:-300}
local start_time
@ -261,6 +456,216 @@ wait_for_cnpg_pods() {
done
}
cluster_exists() {
kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" >/dev/null 2>&1
}
cluster_has_pods() {
kubectl -n "$NAMESPACE" get pods -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" --no-headers 2>/dev/null | grep -q .
}
latest_backup_name() {
kubectl -n "$NAMESPACE" get backup \
--sort-by=.metadata.creationTimestamp \
-o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null | tail -n 1
}
latest_completed_backup_name() {
kubectl -n "$NAMESPACE" get backup \
--sort-by=.metadata.creationTimestamp \
-o jsonpath='{range .items[*]}{.metadata.name}{"|"}{.status.phase}{"\n"}{end}' 2>/dev/null | \
awk -F'|' '{p=tolower($2); if (p=="completed" || p=="succeeded") {name=$1}} END {print name}'
}
wait_for_backup() {
local backup_name="$1"
local start_time now phase phase_lc
start_time=$(date +%s)
while true; do
phase=$(kubectl -n "$NAMESPACE" 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)
echo "Backup $backup_name completed."
return 0
;;
failed|error)
echo "Backup $backup_name failed (phase=$phase)." >&2
return 1
;;
esac
now=$(date +%s)
if (( now - start_time > BACKUP_WAIT_TIMEOUT )); then
echo "Timed out waiting for backup $backup_name." >&2
return 1
fi
echo "Waiting for backup $backup_name to complete (phase=${phase:-unknown}) ..."
sleep 10
done
}
run_garage_backup() {
if [[ ! -x "$SCRIPT_DIR/init_prole-db-backup.sh" ]]; then
echo "WARN: init_prole-db-backup.sh not found; skipping Garage backup." >&2
return 1
fi
echo "Running Garage backup via init_prole-db-backup.sh ..."
if ! "$SCRIPT_DIR/init_prole-db-backup.sh" start; then
echo "Garage backup script failed." >&2
return 1
fi
sleep 2
local backup_name
backup_name=$(latest_backup_name)
if [[ -z "$backup_name" ]]; then
echo "No backup resource detected after triggering backup." >&2
return 1
fi
wait_for_backup "$backup_name"
}
get_primary_pod() {
local primary
primary=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{.status.currentPrimary}' 2>/dev/null || true)
if [[ -z "$primary" ]]; then
primary=$(kubectl -n "$NAMESPACE" get pods -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
fi
printf '%s' "$primary"
}
decode_b64() {
local data="$1"
if [[ -z "$data" ]]; then
return 1
fi
printf '%s' "$data" | base64 -d 2>/dev/null
}
resolve_db_credentials() {
local user_b64 pass_b64 user pass
user_b64=$(kubectl -n "$NAMESPACE" get secret prole-db-user -o jsonpath='{.data.username}' 2>/dev/null || true)
pass_b64=$(kubectl -n "$NAMESPACE" get secret prole-db-user -o jsonpath='{.data.password}' 2>/dev/null || true)
user=$(decode_b64 "$user_b64" || true)
pass=$(decode_b64 "$pass_b64" || true)
if [[ -z "$user" || -z "$pass" ]]; then
user_b64=$(kubectl -n "$NAMESPACE" get secret prole-db-superuser -o jsonpath='{.data.username}' 2>/dev/null || true)
pass_b64=$(kubectl -n "$NAMESPACE" get secret prole-db-superuser -o jsonpath='{.data.password}' 2>/dev/null || true)
user=$(decode_b64 "$user_b64" || true)
pass=$(decode_b64 "$pass_b64" || true)
fi
if [[ -z "$user" || -z "$pass" ]]; then
return 1
fi
printf '%s\n%s' "$user" "$pass"
}
pgdump_local() {
local pod user pass db_name dump_dir dump_file timestamp
pod=$(get_primary_pod)
if [[ -z "$pod" ]]; then
echo "ERROR: No CNPG pod available for pg_dump." >&2
return 1
fi
local creds
if ! creds=$(resolve_db_credentials); then
echo "ERROR: Unable to resolve database credentials for pg_dump." >&2
return 1
fi
user=$(printf '%s' "$creds" | sed -n '1p')
pass=$(printf '%s' "$creds" | sed -n '2p')
db_name=${PROLE_DB_NAME:-prole-db}
dump_dir="$BACKUP_DIR"
mkdir -p "$dump_dir"
timestamp=$(date +%Y%m%d%H%M%S)
dump_file="$dump_dir/${CNPG_CLUSTER_NAME}-pgdump-${timestamp}.dump"
echo "Running pg_dump against pod $pod (db=$db_name) ..."
if kubectl -n "$NAMESPACE" exec "$pod" -c postgres -- env PGPASSWORD="$pass" \
pg_dump -U "$user" -d "$db_name" -Fc > "$dump_file"; then
echo "pg_dump saved to $dump_file"
return 0
fi
echo "pg_dump failed; removing partial file." >&2
rm -f "$dump_file"
return 1
}
attempt_backup_if_active() {
if ! cluster_exists; then
return 0
fi
if ! cluster_has_pods; then
echo "CNPG cluster '$CNPG_CLUSTER_NAME' exists but no pods detected; skipping backup." >&2
return 0
fi
echo "CNPG cluster '$CNPG_CLUSTER_NAME' detected; attempting Garage backup ..."
if run_garage_backup; then
return 0
fi
echo "Garage backup failed; attempting local pg_dump ..." >&2
if pgdump_local; then
return 0
fi
echo "WARN: Both Garage backup and local pg_dump failed." >&2
return 0
}
force_rollout() {
if [[ ! -x "$CNPG_FORCE_ROLLOUT_SCRIPT" ]]; then
echo "WARN: Force rollout script not found at $CNPG_FORCE_ROLLOUT_SCRIPT" >&2
return 1
fi
echo "Attempting force rollout of CNPG pods ..."
"$CNPG_FORCE_ROLLOUT_SCRIPT" rollout
}
reset_and_reinit() {
local backup_name tmp_manifest
backup_name=$(latest_completed_backup_name)
echo "Resetting CNPG cluster '$CNPG_CLUSTER_NAME' in namespace '$NAMESPACE' ..."
kubectl -n "$NAMESPACE" delete cluster "$CNPG_CLUSTER_NAME" --ignore-not-found
kubectl -n "$NAMESPACE" wait --for=delete pod -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" --timeout=180s >/dev/null 2>&1 || true
if [[ -n "$backup_name" && -f "$RECOVERY_TEMPLATE" ]]; then
tmp_manifest=$(mktemp)
sed "s/{{BACKUP_NAME}}/${backup_name}/g" "$RECOVERY_TEMPLATE" > "$tmp_manifest"
echo "Re-initializing from backup $backup_name ..."
CNPG_MANIFEST_OVERRIDE="$tmp_manifest" ensure_prole_stack_resources
rm -f "$tmp_manifest"
else
if [[ -n "$backup_name" && ! -f "$RECOVERY_TEMPLATE" ]]; then
echo "WARN: Recovery template not found: $RECOVERY_TEMPLATE" >&2
fi
if [[ -z "$backup_name" ]]; then
echo "No completed backups found; starting fresh initialization." >&2
fi
ensure_prole_stack_resources
fi
wait_for_cnpg_pods 300
}
# Resolve OpenBao URL: prefer explicit env, then localhost port-forward, then cluster DNS
bao_service_url() {
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
@ -378,7 +783,9 @@ get_latest_cnpg_version() {
initialize() {
ensure_tools
ensure_namespace
attempt_backup_if_active
ensure_cnpg_operator
ensure_barman_plugin
echo "Using namespace: $NAMESPACE"
fetch_admin_keys_and_db_pass_from_bao_or_local
@ -387,7 +794,20 @@ initialize() {
ensure_prole_stack_resources
if ! wait_for_cnpg_pods 300; then
return 1
echo "WARN: CNPG pods did not become ready after init; attempting force rollout ..." >&2
if force_rollout; then
if ! wait_for_cnpg_pods 300; then
echo "WARN: Force rollout did not recover CNPG; attempting reset and re-init ..." >&2
if ! reset_and_reinit; then
return 1
fi
fi
else
echo "WARN: Force rollout failed; attempting reset and re-init ..." >&2
if ! reset_and_reinit; then
return 1
fi
fi
fi
# Support both names for the toggle from prole.cfg
@ -438,6 +858,7 @@ case "$ACTION" in
ensure_tools
ensure_namespace
ensure_cnpg_operator
ensure_barman_plugin
if [[ ! -f "$CNPG_MANIFEST" ]]; then
echo "ERROR: CNPG manifest not found at $CNPG_MANIFEST" >&2
exit 1
@ -485,8 +906,13 @@ case "$ACTION" in
exit 1
fi
;;
install-barman-plugin)
ensure_tools
ensure_cnpg_operator
ensure_barman_plugin
;;
*)
echo "Usage: $0 {create|delete|recreate|start|stop|status|restart|initialize|update|reload} [dbname]" >&2
echo "Usage: $0 {create|delete|recreate|start|stop|status|restart|initialize|update|reload|install-barman-plugin} [dbname]" >&2
exit 2
;;
esac

View File

@ -14,6 +14,14 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh"
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
prole_set_mode "${2:-}"
shift 2
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
prole_set_mode "${1#*=}"
shift
fi
ACTION=${1:-}
if [[ -n "${GARAGE_INIT_LOG:-}" ]]; then

View File

@ -32,6 +32,7 @@ Actions:
Options:
-v, --verbose Enable verbose output
-m, --mode k3d|k3s|k8s (maps to dev|service|prod)
-e, --environment dev|service|prod (default: $ENVIRONMENT)
-h, --host Remote host for service/prod environments
-n, --name k3d cluster name
@ -58,6 +59,24 @@ while [[ $# -gt 0 ]]; do
VERBOSE=true
shift
;;
-m|--mode)
prole_set_mode "${2:-}"
case "${PROLE_MODE:-}" in
k3d) ENVIRONMENT="dev" ;;
k3s) ENVIRONMENT="service" ;;
k8s) ENVIRONMENT="prod" ;;
esac
shift 2
;;
--mode=*|-m=*)
prole_set_mode "${1#*=}"
case "${PROLE_MODE:-}" in
k3d) ENVIRONMENT="dev" ;;
k3s) ENVIRONMENT="service" ;;
k8s) ENVIRONMENT="prod" ;;
esac
shift
;;
-e|--environment)
ENVIRONMENT="$2"
shift 2
@ -85,12 +104,19 @@ if [[ -z "$ACTION" ]]; then
fi
ensure_tools() {
for t in k3d docker; do
command -v "$t" >/dev/null || { echo "ERROR: Missing required tool: $t" >&2; exit 1; }
done
if [[ "$ENVIRONMENT" == "dev" ]]; then
for t in k3d docker; do
command -v "$t" >/dev/null || { echo "ERROR: Missing required tool: $t" >&2; exit 1; }
done
else
command -v kubectl >/dev/null || { echo "ERROR: Missing required tool: kubectl" >&2; exit 1; }
fi
}
ensure_docker() {
if [[ "$ENVIRONMENT" != "dev" ]]; then
return 0
fi
if ! docker info >/dev/null 2>&1; then
echo "ERROR: Docker is not running." >&2
exit 1

View File

@ -20,6 +20,14 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh"
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
prole_set_mode "${2:-}"
shift 2
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
prole_set_mode "${1#*=}"
shift
fi
ACTION=${1:-initialize}
CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-prole-db}
@ -49,6 +57,11 @@ ensure_tools() {
done
}
cnpg_supports_pod_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
}
sha256_stdin() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum | awk '{print $1}'
@ -288,6 +301,10 @@ 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."
return 1
fi
local conf conf_hash
conf=$(get_krb5_conf)

View File

@ -10,23 +10,35 @@ set -euo pipefail
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
ACTION=${1:-test}
ORIG_ARGS=("$@")
# Load env
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$PROLE_HOME/env.sh"
set -- "${ORIG_ARGS[@]}"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$HOME/.prole/env.sh"
set -- "${ORIG_ARGS[@]}"
fi
# Load config values from prole.cfg before applying defaults/CLI overrides.
# shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh"
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
prole_set_mode "${2:-}"
shift 2
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
prole_set_mode "${1#*=}"
shift
fi
ACTION=${1:-test}
NAMESPACE=${NAMESPACE:-default}
KRB5_REALM=${KRB5_REALM:-${REALM:-}}
KRB5_KDC=${KRB5_KDC:-}

View File

@ -13,6 +13,14 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh"
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
prole_set_mode "${2:-}"
shift 2
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
prole_set_mode "${1#*=}"
shift
fi
if [[ -z "${PROLE_SERVICE:-}" ]]; then
echo "ERROR: PROLE_SERVICE is not defined. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2
exit 1

View File

@ -51,6 +51,13 @@ root_token_file="$SECRETS_DIR/openbao-root-token"
while [[ $# -gt 0 ]]; do
case "$1" in
-m|--mode)
shift
prole_set_mode "${1:-}"
;;
-m=*|--mode=*)
prole_set_mode "${1#*=}"
;;
-n|--namespace)
shift
if [[ -z "${1:-}" ]]; then

207
etc/init_opentofu.sh Executable file
View File

@ -0,0 +1,207 @@
#!/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)
# Load environment and config via prole_cfg.sh
# shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh"
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
ACTION=""
NAMESPACE_OVERRIDE=""
OPENTOFU_NAME=${OPENTOFU_NAME:-opentofu}
OPENTOFU_ADMIN_USER=${OPENTOFU_ADMIN_USER:-admin}
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
while [[ $# -gt 0 ]]; do
case "$1" in
-m|--mode)
shift
prole_set_mode "${1:-}"
;;
-m=*|--mode=*)
prole_set_mode "${1#*=}"
;;
-n|--namespace)
shift
if [[ -z "${1:-}" ]]; then
echo "ERROR: -n/--namespace requires a value" >&2
exit 2
fi
NAMESPACE_OVERRIDE="$1"
;;
-n=*|--namespace=*)
NAMESPACE_OVERRIDE="${1#*=}"
;;
start|stop|status|restart|initialize|update|reload)
ACTION="$1"
;;
--)
shift
break
;;
*)
if [[ -z "$ACTION" && "$1" != -* ]]; then
ACTION="$1"
else
echo "Unknown argument: $1" >&2
exit 2
fi
;;
esac
shift
done
if [[ -n "$NAMESPACE_OVERRIDE" ]]; then
NAMESPACE="$NAMESPACE_OVERRIDE"
export NAMESPACE
fi
if [[ -z "${NAMESPACE:-}" ]]; then
NAMESPACE="default"
export NAMESPACE
fi
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 "$NAMESPACE" >/dev/null 2>&1; then
echo "Creating namespace '$NAMESPACE' ..."
kubectl create namespace "$NAMESPACE" >/dev/null 2>&1 || true
fi
}
openbao_url() {
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
echo "$PROLE_OPENBAO_URL"
elif curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:18200"
else
echo "http://openbao.${NAMESPACE}.svc.cluster.local:8200"
fi
}
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" ]]; 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() {
if [[ -z "${OPENTOFU_ADMIN_PASSWORD:-}" ]]; then
OPENTOFU_ADMIN_PASSWORD="${DB_PASSWORD:-}"
fi
if [[ -z "${OPENTOFU_ADMIN_PASSWORD:-}" ]] || is_anchor "${OPENTOFU_ADMIN_PASSWORD:-}"; then
local fetched
fetched=$(fetch_openbao_secret "prole/${NAMESPACE:-default}/db" "password")
if [[ -n "$fetched" && "$fetched" != "null" ]]; then
OPENTOFU_ADMIN_PASSWORD="$fetched"
fi
fi
if [[ -z "${OPENTOFU_ADMIN_PASSWORD:-}" ]]; then
echo "ERROR: OPENTOFU_ADMIN_PASSWORD/DB_PASSWORD is missing." >&2
exit 1
fi
}
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 "$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 '$NAMESPACE' ..."
kubectl apply -n "$NAMESPACE" -f "$OPENTOFU_MANIFEST_DIR/deployment.yaml"
kubectl rollout status deploy/$OPENTOFU_NAME -n "$NAMESPACE" --timeout=120s || true
}
delete_k8s() {
echo "Removing OpenTofu resources from namespace '$NAMESPACE' ..."
kubectl delete -n "$NAMESPACE" -f "$OPENTOFU_MANIFEST_DIR/deployment.yaml" --ignore-not-found
kubectl delete -n "$NAMESPACE" secret opentofu-admin --ignore-not-found || true
}
status_k8s() {
kubectl -n "$NAMESPACE" get deploy "$OPENTOFU_NAME" 2>/dev/null || true
kubectl -n "$NAMESPACE" get svc "$OPENTOFU_NAME" 2>/dev/null || true
}
case "${ACTION:-}" in
start|initialize|update|reload|restart)
ensure_tools
ensure_namespace
resolve_admin_password
ensure_opentofu_secret
apply_k8s
;;
stop)
ensure_tools
delete_k8s
;;
status)
ensure_tools
status_k8s
;;
*)
echo "Usage: $0 [start|stop|status|restart|initialize|update|reload] [-n namespace] [--mode k3d|k3s|k8s]" >&2
exit 1
;;
esac

View File

@ -131,11 +131,16 @@ if [ -z "${PF_MANAGEMENT_NAMESPACE:-}" ] && [ -n "${NAMESPACE:-}" ]; then
PF_MANAGEMENT_NAMESPACE="$NAMESPACE"
fi
# SUPABASE_NAMESPACE="${SUPABASE_NAMESPACE:-supabase}"
# SUPABASE_DB_TARGET="${SUPABASE_DB_TARGET:-${SUPABASE_DB_SERVICE:-svc/db}}"
SUPABASE_NAMESPACE="${SUPABASE_NAMESPACE:-supabase}"
SUPABASE_DB_SERVICE="${SUPABASE_DB_SERVICE:-db}"
SUPABASE_DB_TARGET="${SUPABASE_DB_TARGET:-svc/$SUPABASE_DB_SERVICE}"
# Supabase logic removed as it's handled in another script
SUPABASE_ENABLED_EFFECTIVE=0
if is_truthy "${SUPABASE_ENABLED:-}"; then
SUPABASE_ENABLED_EFFECTIVE=1
elif is_truthy "${init_cluster_supabase_enabled:-}"; then
SUPABASE_ENABLED_EFFECTIVE=1
fi
PROLE_DB_ALT_PORT_BASE="${PROLE_DB_ALT_PORT_BASE:-15432}"
PROLE_DB_ALT_PORT_EFFECTIVE=""
@ -212,6 +217,20 @@ validate_env() {
fi
}
ensure_supabase_ports() {
[ "$SUPABASE_ENABLED_EFFECTIVE" -eq 1 ] || return 0
local wire_script="$SCRIPT_DIR/init_supabase_ports.sh"
if [ ! -x "$wire_script" ]; then
warn "Supabase port wiring script not found/executable: $wire_script"
return 0
fi
log "Ensuring Supabase DB wiring (db -> prole-db-rw, supabase-postgres -> 15432) ..."
SUPABASE_NAMESPACE="$SUPABASE_NAMESPACE" \
"$wire_script" -n "$NAMESPACE" --supabase-namespace "$SUPABASE_NAMESPACE"
}
# ---- Preflight: Docker + k3d awareness ----
# Return 0 when Docker CLI can talk to a running daemon.
docker_is_running() {
@ -287,6 +306,13 @@ ensure_k3d_ready_if_applicable() {
fi
}
is_local_mode() {
case "${PROLE_MODE:-}" in
""|k3d) return 0 ;;
esac
return 1
}
pid_file_for() { printf '%s/%s.pid' "$PID_DIR" "$1"; }
log_file_for() { printf '%s/%s.log' "$LOG_DIR" "$1"; }
@ -418,7 +444,10 @@ foreach_mapping() {
if [ -n "${DB_HOST_PORT:-}" ]; then
hostPort="$DB_HOST_PORT"
fi
# Supabase override removed
if [ "$SUPABASE_ENABLED_EFFECTIVE" -eq 1 ]; then
ns="$SUPABASE_NAMESPACE"
target="$SUPABASE_DB_TARGET"
fi
fi
# prole-db logic removed, now uses XML
@ -602,9 +631,12 @@ scan_for_collisions() {
do_start() {
validate_env
# Preflight: require Docker daemon and k3d (if applicable)
ensure_docker_running
ensure_k3d_ready_if_applicable
if is_local_mode; then
# Preflight: require Docker daemon and k3d (if applicable)
ensure_docker_running
ensure_k3d_ready_if_applicable
fi
ensure_supabase_ports
vlog "Scanning for port collisions..."
foreach_mapping scan_for_collisions
@ -620,9 +652,12 @@ do_stop() {
do_restart() {
validate_env
# Preflight: require Docker daemon and k3d (if applicable)
ensure_docker_running
ensure_k3d_ready_if_applicable
if is_local_mode; then
# Preflight: require Docker daemon and k3d (if applicable)
ensure_docker_running
ensure_k3d_ready_if_applicable
fi
ensure_supabase_ports
vlog "Scanning for port collisions (excluding our own)..."
# During restart, we'll stop them first anyway, but let's be safe.
@ -702,6 +737,15 @@ while [ $# -gt 0 ]; do
fi
shift
;;
-m|--mode)
shift
prole_set_mode "${1:-}"
shift
;;
-m=*|--mode=*)
prole_set_mode "${1#*=}"
shift
;;
-v|--verbose)
VERBOSE=1
shift

View File

@ -4,7 +4,7 @@ set -euo pipefail
# init_prole-db-backup.sh
# Purpose:
# - Configure CloudNative-PG to backup to Garage (S3-compatible)
# - Configure CloudNative-PG to backup to Garage (S3-compatible) via Barman Cloud Plugin
# - Create initial backup
# Initialize SCRIPT_DIR
@ -14,6 +14,14 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh"
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
prole_set_mode "${2:-}"
shift 2
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
prole_set_mode "${1#*=}"
shift
fi
ACTION=${1:-start}
NAMESPACE=${NAMESPACE:-default}
@ -25,6 +33,9 @@ GARAGE_BACKUP_SECRET_NAME=${GARAGE_BACKUP_SECRET_NAME:-prole-db-barman-s3}
GARAGE_S3_ENDPOINT=${GARAGE_S3_ENDPOINT:-http://$GARAGE_NAME.$NAMESPACE.svc.cluster.local:3900}
GARAGE_S3_REGION=${GARAGE_S3_REGION:-garage}
RUN_FIRST_BACKUP=${RUN_FIRST_BACKUP:-1}
RETENTION_POLICY=${RETENTION_POLICY:-30d}
BARMAN_PLUGIN_NAME=${BARMAN_PLUGIN_NAME:-barman-cloud.cloudnative-pg.io}
BARMAN_OBJECT_NAME=${BARMAN_OBJECT_NAME:-prole-db-barman-objectstore}
usage() {
cat <<USAGE
@ -58,6 +69,10 @@ ensure_cluster() {
fi
}
barman_crd_ready() {
kubectl get crd objectstores.barmancloud.cnpg.io >/dev/null 2>&1
}
get_garage_pod() {
kubectl get pods -n "$NAMESPACE" -l "app=$GARAGE_NAME" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true
}
@ -137,26 +152,82 @@ ensure_garage_bucket_and_key() {
--dry-run=client -o yaml | kubectl apply -f -
}
configure_cnpg_backup() {
echo "Configuring CNPG backup to use Garage bucket '$GARAGE_BACKUP_BUCKET' ..."
kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{
\"spec\": {
\"backup\": {
\"barmanObjectStore\": {
\"destinationPath\": \"s3://$GARAGE_BACKUP_BUCKET/\",
\"endpointURL\": \"$GARAGE_S3_ENDPOINT\",
\"s3Credentials\": {
\"accessKeyId\": {\"name\": \"$GARAGE_BACKUP_SECRET_NAME\", \"key\": \"ACCESS_KEY_ID\"},
\"secretAccessKey\": {\"name\": \"$GARAGE_BACKUP_SECRET_NAME\", \"key\": \"SECRET_ACCESS_KEY\"},
\"region\": {\"name\": \"$GARAGE_BACKUP_SECRET_NAME\", \"key\": \"REGION\"}
},
\"wal\": {\"compression\": \"gzip\"},
\"data\": {\"compression\": \"gzip\"}
},
\"retentionPolicy\": \"30d\"
apply_barman_object_store() {
if ! barman_crd_ready; then
echo "ERROR: Barman Cloud plugin CRD not found (objectstores.barmancloud.cnpg.io). Install the plugin first." >&2
exit 1
fi
echo "Applying Barman Cloud ObjectStore '$BARMAN_OBJECT_NAME' ..."
kubectl apply -n "$NAMESPACE" -f - <<OBJECTSTORE
apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
name: $BARMAN_OBJECT_NAME
spec:
retentionPolicy: $RETENTION_POLICY
configuration:
destinationPath: s3://$GARAGE_BACKUP_BUCKET/
endpointURL: $GARAGE_S3_ENDPOINT
s3Credentials:
accessKeyId:
name: $GARAGE_BACKUP_SECRET_NAME
key: ACCESS_KEY_ID
secretAccessKey:
name: $GARAGE_BACKUP_SECRET_NAME
key: SECRET_ACCESS_KEY
region:
name: $GARAGE_BACKUP_SECRET_NAME
key: REGION
wal:
compression: gzip
data:
compression: gzip
OBJECTSTORE
}
ensure_barman_plugin_config() {
local plugin_names plugin_present
plugin_names=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{.spec.plugins[*].name}' 2>/dev/null || true)
plugin_present=$(printf '%s\n' "$plugin_names" | tr ' ' '\n' | grep -F "$BARMAN_PLUGIN_NAME" || true)
if [[ -n "$plugin_present" ]]; then
return 0
fi
local plugins_json
plugins_json=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{.spec.plugins}' 2>/dev/null || true)
echo "Configuring CNPG to use Barman Cloud plugin '$BARMAN_PLUGIN_NAME' ..."
if [[ -z "$plugins_json" || "$plugins_json" == "null" || "$plugins_json" == "[]" ]]; then
kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{
\"spec\": {
\"plugins\": [
{
\"name\": \"$BARMAN_PLUGIN_NAME\",
\"isWALArchiver\": true,
\"parameters\": {\"barmanObjectName\": \"$BARMAN_OBJECT_NAME\"}
}
]
}
}
}"
}"
else
kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type json -p "[
{
\"op\": \"add\",
\"path\": \"/spec/plugins/-\",
\"value\": {
\"name\": \"$BARMAN_PLUGIN_NAME\",
\"isWALArchiver\": true,
\"parameters\": {\"barmanObjectName\": \"$BARMAN_OBJECT_NAME\"}
}
}
]"
fi
}
remove_native_barman_config() {
kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{\"spec\":{\"backup\":null}}" >/dev/null 2>&1 || true
}
trigger_backup() {
@ -169,6 +240,11 @@ kind: Backup
metadata:
name: $backup_name
spec:
method: plugin
pluginConfiguration:
name: $BARMAN_PLUGIN_NAME
parameters:
barmanObjectName: $BARMAN_OBJECT_NAME
cluster:
name: $CNPG_CLUSTER_NAME
BACKUP
@ -186,7 +262,9 @@ case "$ACTION" in
ensure_namespace
ensure_cluster
ensure_garage_bucket_and_key
configure_cnpg_backup
apply_barman_object_store
ensure_barman_plugin_config
remove_native_barman_config
if [[ "$RUN_FIRST_BACKUP" == "1" ]]; then
trigger_backup
fi

View File

@ -19,6 +19,14 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh"
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
prole_set_mode "${2:-}"
shift 2
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
prole_set_mode "${1#*=}"
shift
fi
ACTION=${1:-reset}
NAMESPACE=${NAMESPACE:-default}

View File

@ -13,6 +13,14 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh"
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
prole_set_mode "${2:-}"
shift 2
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
prole_set_mode "${1#*=}"
shift
fi
ACTION=${1:-}
VERSION=${2:-latest}
@ -34,6 +42,19 @@ ensure_tools() {
done
}
ensure_barman_plugin() {
if kubectl get crd objectstores.barmancloud.cnpg.io >/dev/null 2>&1; then
return 0
fi
echo "Barman Cloud plugin CRD not found; installing plugin..."
if [[ -x "$SCRIPT_DIR/init_cloudnative_pg.sh" ]]; then
bash "$SCRIPT_DIR/init_cloudnative_pg.sh" install-barman-plugin
else
echo "ERROR: init_cloudnative_pg.sh not found; cannot install Barman Cloud plugin." >&2
exit 1
fi
}
ensure_namespace() {
if ! kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then
echo "Creating namespace '$NAMESPACE' ..."
@ -102,9 +123,11 @@ start() {
echo "Checking dependencies..."
# 1. k3d is running
if ! command -v k3d >/dev/null || ! k3d cluster list >/dev/null 2>&1; then
echo "ERROR: k3d is not running or not installed." >&2
exit 1
if [[ -z "${PROLE_MODE:-}" || "${PROLE_MODE}" == "k3d" ]]; then
if ! command -v k3d >/dev/null || ! k3d cluster list >/dev/null 2>&1; then
echo "ERROR: k3d is not running or not installed." >&2
exit 1
fi
fi
# 2. cnpg operator is loaded
@ -112,6 +135,7 @@ start() {
echo "ERROR: CloudNative-PG operator is not loaded." >&2
exit 1
fi
ensure_barman_plugin
# 3. openbao is configured (local container)
local openbao_url="${PROLE_OPENBAO_URL:-http://127.0.0.1:18200}"
@ -204,6 +228,7 @@ restart() {
deploy() {
ensure_tools
ensure_namespace
ensure_barman_plugin
local image
if [[ "$VERSION" == "latest" ]]; then
image=$(get_latest_image)

View File

@ -16,6 +16,14 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh"
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
prole_set_mode "${2:-}"
shift 2
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
prole_set_mode "${1#*=}"
shift
fi
SUPABASE_NAMESPACE="${SUPABASE_NAMESPACE:-supabase}"
PROLE_DB_SERVICE="${PROLE_DB_SERVICE:-prole-db-rw}"
SUPABASE_DB_SERVICE="${SUPABASE_DB_SERVICE:-db}"
@ -112,8 +120,10 @@ selector_pairs_to_yaml() {
}
get_selector_pairs() {
kubectl -n "$SUPABASE_NAMESPACE" get svc "$1" \
-o jsonpath='{range $k,$v := .spec.selector}{$k}={$v},{end}' 2>/dev/null | sed 's/,$//'
local out
out=$(kubectl -n "$SUPABASE_NAMESPACE" get svc "$1" \
-o jsonpath='{range $k,$v := .spec.selector}{$k}={$v},{end}' 2>/dev/null || true)
printf '%s' "$out" | sed 's/,$//'
}
restart_supabase() {
@ -224,7 +234,8 @@ metadata:
spec:
type: ClusterIP
selector:
${selector_yaml} ports:
${selector_yaml}
ports:
- name: postgres
port: ${SUPABASE_POSTGRES_PORT}
targetPort: ${DB_PORT}

View File

@ -84,6 +84,40 @@ _prole_cfg_extract_key() {
printf '%s' "$value"
}
prole_normalize_mode() {
local s
s=$(printf '%s' "${1:-}" | tr 'A-Z' 'a-z')
case "$s" in
dev|k3d|k3d-*) echo "k3d"; return 0 ;;
service|k3s|k3s-*) echo "k3s"; return 0 ;;
prod|production|k8s|k8s-*) echo "k8s"; return 0 ;;
prole-dev-cluster|k3d-prole-dev-cluster) echo "k3d"; return 0 ;;
prole-service-cluster) echo "k3s"; return 0 ;;
prole-prod-cluster) echo "k8s"; return 0 ;;
esac
echo "$s"
}
prole_set_mode() {
local raw="$1"
if [[ -z "$raw" ]]; then
echo "ERROR: --mode requires a value (k3d, k3s, k8s)" >&2
exit 2
fi
local norm
norm=$(prole_normalize_mode "$raw")
case "$norm" in
k3d|k3s|k8s)
PROLE_MODE="$norm"
export PROLE_MODE
;;
*)
echo "ERROR: Unsupported mode '$raw' (use k3d, k3s, or k8s)" >&2
exit 2
;;
esac
}
_prole_cfg_file=""
if [[ -n "${PROLE_CONF:-}" && -f "$PROLE_CONF/prole.cfg" ]]; then
_prole_cfg_file="$PROLE_CONF/prole.cfg"
@ -127,6 +161,22 @@ if [[ -z "${NAMESPACE:-}" ]]; then
fi
export NAMESPACE=${NAMESPACE:-default}
if [[ -z "${PROLE_MODE:-}" ]]; then
if [[ -n "${DEPLOYMENT_MODE:-}" ]]; then
_prole_mode_guess=$(prole_normalize_mode "$DEPLOYMENT_MODE")
case "$_prole_mode_guess" in
k3d|k3s|k8s) PROLE_MODE="$_prole_mode_guess"; export PROLE_MODE ;;
esac
unset _prole_mode_guess
elif [[ -n "${CLUSTER_ENV:-}" ]]; then
_prole_mode_guess=$(prole_normalize_mode "$CLUSTER_ENV")
case "$_prole_mode_guess" in
k3d|k3s|k8s) PROLE_MODE="$_prole_mode_guess"; export PROLE_MODE ;;
esac
unset _prole_mode_guess
fi
fi
unset _prole_cfg_script_dir
unset _prole_cfg_home_guess
unset _prole_cfg_file

View File

@ -0,0 +1,16 @@
---
- name: Start k3s on a single host
hosts: k3s_hosts
become: true
serial: 1
pre_tasks:
- name: Require a single host target
ansible.builtin.assert:
that:
- ansible_play_hosts_all | length == 1
fail_msg: "This playbook targets one host. Use --limit <host>."
tasks:
- name: Start k3s service
ansible.builtin.import_role:
name: k3s
tasks_from: start

View File

@ -0,0 +1,33 @@
---
- name: Stop k3s agents first
hosts: k3s_hosts
become: true
serial: 1
tasks:
- name: Stop k3s services on agent nodes
ansible.builtin.import_role:
name: k3s
tasks_from: stop
when: k3s_role != "server"
- name: Stop non-init k3s servers
hosts: k3s_hosts
become: true
serial: 1
tasks:
- name: Stop k3s services on non-init servers
ansible.builtin.import_role:
name: k3s
tasks_from: stop
when: k3s_role == "server" and not (k3s_cluster_init | bool)
- name: Stop init k3s servers last
hosts: k3s_hosts
become: true
serial: 1
tasks:
- name: Stop k3s services on init servers
ansible.builtin.import_role:
name: k3s
tasks_from: stop
when: k3s_role == "server" and (k3s_cluster_init | bool)

View File

@ -0,0 +1,28 @@
---
- name: Set k3s service name
ansible.builtin.set_fact:
k3s_service_name: "{{ 'k3s' if k3s_role == 'server' else 'k3s-agent' }}"
- name: Gather service facts
ansible.builtin.service_facts:
- name: Ensure k3s service is installed
ansible.builtin.assert:
that:
- (k3s_service_name + ".service") in ansible_facts.services
fail_msg: "k3s service {{ k3s_service_name }} is not installed. Run the k3s role or reset playbook first."
- name: Start k3s service
ansible.builtin.systemd:
name: "{{ k3s_service_name }}"
state: started
enabled: true
no_block: true
- name: Wait for k3s to become active
ansible.builtin.command: "systemctl is-active {{ k3s_service_name }}"
register: k3s_service_state
changed_when: false
retries: "{{ [1, (k3s_start_timeout_seconds | default(120) | int) // 10] | max }}"
delay: 10
until: k3s_service_state.stdout == "active"

View File

@ -0,0 +1,13 @@
---
- name: Gather service facts
ansible.builtin.service_facts:
- name: Stop k3s services if present
ansible.builtin.service:
name: "{{ item }}"
state: stopped
enabled: false
loop:
- k3s
- k3s-agent
when: (item + ".service") in ansible_facts.services

View File

@ -474,6 +474,32 @@ def _extract_yaml_scalar_from_text(text: str, key: str) -> str:
return ''
def _extract_inline_vault_block(text: str, key: str) -> str:
lines = text.splitlines()
for idx, raw in enumerate(lines):
stripped = raw.strip()
if not stripped or stripped.startswith('#'):
continue
if stripped.startswith(f"{key}:") and "!vault" in stripped:
base_indent = len(raw) - len(raw.lstrip())
block = []
i = idx + 1
while i < len(lines):
line = lines[i]
if not line.strip():
i += 1
continue
indent = len(line) - len(line.lstrip())
if indent <= base_indent:
break
block.append(line.strip())
i += 1
if block and block[0].startswith("$ANSIBLE_VAULT"):
return "\n".join(block) + "\n"
return ""
return ""
def _try_read_ansible_vault_value(vault_path: Path, key: str) -> str:
if not vault_path.exists():
return ''
@ -499,9 +525,8 @@ def _try_read_ansible_vault_value(vault_path: Path, key: str) -> str:
return ''
tmp_path = None
args = ['ansible-vault', 'view', str(vault_path)]
if password_file:
args += ['--vault-password-file', password_file]
password_file = password_file
elif password:
try:
tmp = tempfile.NamedTemporaryFile(delete=False)
@ -509,14 +534,14 @@ def _try_read_ansible_vault_value(vault_path: Path, key: str) -> str:
tmp.flush()
tmp.close()
tmp_path = tmp.name
args += ['--vault-password-file', tmp_path]
password_file = tmp_path
except Exception:
tmp_path = None
return ''
try:
def _vault_view(path: str) -> str:
res = subprocess.run(
args,
['ansible-vault', 'view', path] + (['--vault-password-file', password_file] if password_file else []),
capture_output=True,
text=True,
timeout=5,
@ -525,7 +550,31 @@ def _try_read_ansible_vault_value(vault_path: Path, key: str) -> str:
)
if res.returncode != 0:
return ''
return _extract_yaml_scalar_from_text(res.stdout or '', key)
return res.stdout or ''
try:
output = _vault_view(str(vault_path))
if output:
return _extract_yaml_scalar_from_text(output, key)
# Inline vault: extract the block and decrypt separately.
try:
raw_text = vault_path.read_text()
except Exception:
raw_text = ''
inline_block = _extract_inline_vault_block(raw_text, key)
if not inline_block:
return ''
tmp_inline = tempfile.NamedTemporaryFile(delete=False)
tmp_inline.write(inline_block.encode('utf-8'))
tmp_inline.flush()
tmp_inline.close()
output = _vault_view(tmp_inline.name)
try:
os.unlink(tmp_inline.name)
except Exception:
pass
return output.strip()
except Exception:
return ''
finally:
@ -2975,7 +3024,8 @@ class ProleInstaller:
'DEPLOYMENT_MODE': mode,
'DEPLOYMENT_TARGET': target_label,
'NAMESPACE': (self.db_namespace.get() or '').strip(),
'DB_HOST_PORT': (self.db_host_port.get() or '5432').strip()
'DB_HOST_PORT': (self.db_host_port.get() or '5432').strip(),
'PROLE_OPENTOFU_URL': _default_opentofu_pipeline_url(),
}
# Add any other values already in self.prole_cfg_data['Global']
globals_to_save.update(self.prole_cfg_data.get('Global', {}))
@ -9589,6 +9639,9 @@ class ProleSilentInstaller:
def _get_input_bool(self, key: str, default: bool = False) -> bool:
return _parse_bool(self._get_input(key, None), default=default)
def _deployment_mode(self) -> str:
return _deployment_mode_from_env(self._get_input('init_cluster.cluster_env', ''))
def _secret_namespace(self) -> str:
ns = (self._get_input('init_password.db_namespace', '') or '').strip()
if not ns:
@ -10168,7 +10221,8 @@ class ProleSilentInstaller:
'DB_HOST_PORT': (self._get_input('init_password.db_host_port', '5432') or '5432').strip(),
'DOCKER_IMPORT_DIR': self.docker_import_dir or '',
'PROLE_K3S_SERVER': (self._get_input('init_cluster.k3s_server_url', '') or '').strip(),
'PROLE_K3S_TOKEN': _encrypt_cfg_secret(self._get_input('init_cluster.k3s_token', '') or '')
'PROLE_K3S_TOKEN': _encrypt_cfg_secret(self._get_input('init_cluster.k3s_token', '') or ''),
'PROLE_OPENTOFU_URL': _default_opentofu_pipeline_url(),
}
globals_to_save.update(self.prole_cfg_data.get('Global', {}))
globals_to_save['DB_PASSWORD'] = self._secret_cfg_value('Global', 'DB_PASSWORD', db_pw, 'db', 'password')
@ -10327,11 +10381,14 @@ class ProleSilentInstaller:
'PROLE_DATA': self._get_input('env_setup.PROLE_DATA', ''),
'PROLE_LOGS': self._get_input('env_setup.PROLE_LOGS', ''),
'PROLE_SERVICE': self._get_input('env_setup.PROLE_SERVICE', ''),
'PROLE_OPENTOFU_URL': (os.environ.get('PROLE_OPENTOFU_URL') or '').strip(),
}
defaults = self._env_defaults(self._get_input('env_setup.NAMESPACE', ''))
for k in vals:
if not vals[k]:
vals[k] = defaults.get(k, '')
if not vals.get('PROLE_OPENTOFU_URL'):
vals['PROLE_OPENTOFU_URL'] = _default_opentofu_pipeline_url()
vals['NAMESPACE'] = self._get_input('env_setup.NAMESPACE', '')
if not vals.get('PROLE_HOME'):
@ -10343,7 +10400,7 @@ class ProleSilentInstaller:
self._save_env_to_file(vals)
self.reload_env_from_shell()
for k in ('PROLE_HOME', 'PROLE_CONF', 'PROLE_DATA', 'PROLE_LOGS', 'PROLE_SERVICE'):
for k in ('PROLE_HOME', 'PROLE_CONF', 'PROLE_DATA', 'PROLE_LOGS', 'PROLE_SERVICE', 'PROLE_OPENTOFU_URL'):
self.prole_cfg_data['System Environment'][k] = vals[k]
def _step_init_password(self) -> None:

View File

@ -115,6 +115,8 @@ def get_docker_build_platform_args(target_env: Optional[str] = None) -> list[str
if override:
return ["--platform", override]
env_key = (target_env or "").strip().lower()
if env_key == "dev" and is_apple_silicon():
return ["--platform", "linux/arm64"]
if env_key in ("service", "k3s", "prole-service-cluster"):
return ["--platform", "linux/arm64"]
if is_apple_silicon():

View File

@ -13,6 +13,7 @@ import os
import subprocess
import getpass
import json
import tempfile
from pathlib import Path
from typing import Optional, Callable
@ -48,7 +49,7 @@ class ProleNcursesInstaller:
self.selected_env_index = 0
self.kubectx_list = self._get_kubectx_list()
self.selected_kubectx_index = 0
self.k3s_services_status = {"registry": "Unknown", "openbao": "Unknown"}
self.k3s_services_status = {"registry": "Unknown", "openbao": "Unknown", "opentofu": "Unknown"}
self.prod_artifacts_path = InputField(self.main_content_win, 15, 4, 50)
self.prod_artifacts_path.set_value(str(self.project_root / "data" / "staging"))
@ -318,13 +319,42 @@ class ProleNcursesInstaller:
pass
return ["default"]
def _write_k3s_kubeconfig(self, server: str, token: str) -> Path:
if not server.startswith("http"):
server = f"https://{server}"
cfg = (
"apiVersion: v1\n"
"kind: Config\n"
"clusters:\n"
"- cluster:\n"
f" server: {server}\n"
" insecure-skip-tls-verify: true\n"
" name: prole-k3s\n"
"contexts:\n"
"- context:\n"
" cluster: prole-k3s\n"
" user: prole-k3s\n"
" name: prole-k3s\n"
"current-context: prole-k3s\n"
"users:\n"
"- name: prole-k3s\n"
" user:\n"
f" token: {token}\n"
)
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".kubeconfig")
tmp.write(cfg.encode("utf-8"))
tmp.flush()
tmp.close()
os.chmod(tmp.name, 0o600)
return Path(tmp.name)
def _verify_k3s_services(self):
"""Verify registry:2 and openbao on remote k3s cluster."""
"""Verify registry:2, OpenBao, and OpenTofu on remote k3s cluster."""
def _verify():
self.k3s_services_status = {"registry": "Checking...", "openbao": "Checking..."}
self.k3s_services_status = {"registry": "Checking...", "openbao": "Checking...", "opentofu": "Checking..."}
if not self.k3s_token:
self.k3s_services_status = {"registry": "Missing token", "openbao": "Missing token"}
self.k3s_services_status = {"registry": "Missing token", "openbao": "Missing token", "opentofu": "Missing token"}
return
base_cmd = [
@ -348,23 +378,47 @@ class ProleNcursesInstaller:
self.k3s_services_status["openbao"] = "Good"
else:
self.k3s_services_status["openbao"] = "Failing"
# Check opentofu
if "opentofu" in res.stdout.lower():
self.k3s_services_status["opentofu"] = "Good"
else:
self.k3s_services_status["opentofu"] = "Failing"
except Exception as e:
self.status_message = f"K3s connection error: {str(e)}"
self.k3s_services_status = {"registry": "Error", "openbao": "Error"}
self.k3s_services_status = {"registry": "Error", "openbao": "Error", "opentofu": "Error"}
threading.Thread(target=_verify).start()
def _deploy_k3s_services(self):
"""Deploy registry and openbao to remote k3s cluster."""
"""Deploy registry, OpenBao, and OpenTofu to remote k3s cluster."""
def _deploy():
self.status_message = "Deploying services to remote k3s..."
# In a real scenario, we'd run a script or apply manifests
# base_cmd = [...]
# subprocess.run(base_cmd + ["apply", "-f", ...])
# Simulate deployment delay
import time
time.sleep(2)
if not self.k3s_server or not self.k3s_token:
self.status_message = "Missing k3s server or token."
return
env = os.environ.copy()
env["PROLE_HOME"] = str(self.project_root)
env["PROLE_SERVICE"] = str(self.project_root)
env["NAMESPACE"] = self.db_namespace or "default"
env["DB_PASSWORD"] = self.db_password or ""
env["OPENTOFU_ADMIN_PASSWORD"] = self.db_password or ""
env["PROLE_MODE"] = "k3s"
kubeconfig_path = None
try:
kubeconfig_path = self._write_k3s_kubeconfig(self.k3s_server, self.k3s_token)
env["KUBECONFIG"] = str(kubeconfig_path)
self.controller.run_script("init_openbao.sh", args=["-n", env["NAMESPACE"], "update"], env=env)
self.controller.run_script("init_opentofu.sh", args=["-n", env["NAMESPACE"], "update"], env=env)
finally:
if kubeconfig_path:
try:
os.unlink(kubeconfig_path)
except Exception:
pass
self._verify_k3s_services()
self.status_message = "Deployment triggered on remote k3s."

View File

@ -12,8 +12,8 @@ data:
[realms]
PROLE.ORG = {
kdc = 10.0.0.3
admin_server = 10.0.0.3
kdc = kdc.prole.org
admin_server = kdc.prole.org
}
[domain_realm]

View File

@ -0,0 +1,111 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: opentofu-nginx
data:
default.conf: |
server {
listen 8080;
server_name _;
auth_basic "OpenTofu";
auth_basic_user_file /etc/nginx/auth/auth;
location / {
root /usr/share/nginx/html;
index index.html;
}
}
index.html: |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OpenTofu</title>
<style>
body { font-family: "SF Pro Text", "Segoe UI", sans-serif; background: #f5f5f0; color: #1d1d1f; }
.wrap { max-width: 720px; margin: 60px auto; padding: 32px; background: #fff; border-radius: 16px; box-shadow: 0 8px 24px rgba(0,0,0,0.08); }
h1 { margin-top: 0; font-size: 28px; }
code { background: #f2f2f2; padding: 2px 6px; border-radius: 6px; }
</style>
</head>
<body>
<div class="wrap">
<h1>OpenTofu Pipeline Ready</h1>
<p>This service hosts the OpenTofu control plane for Prole deployments.</p>
<p>Pipeline root (on disk): <code>deploy/opentofu/k3s</code></p>
</div>
</body>
</html>
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: opentofu
labels:
app: opentofu
spec:
replicas: 1
selector:
matchLabels:
app: opentofu
template:
metadata:
labels:
app: opentofu
spec:
containers:
- name: opentofu-ui
image: nginx:1.27-alpine
ports:
- name: http
containerPort: 8080
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/conf.d
- name: ui-content
mountPath: /usr/share/nginx/html
- name: opentofu-auth
mountPath: /etc/nginx/auth
readOnly: true
- name: opentofu-runner
image: ghcr.io/opentofu/opentofu:latest
command: ["sh", "-c", "tofu version && tail -f /dev/null"]
env:
- name: OPENTOFU_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: opentofu-admin
key: admin_password
volumes:
- name: nginx-config
configMap:
name: opentofu-nginx
items:
- key: default.conf
path: default.conf
- name: ui-content
configMap:
name: opentofu-nginx
items:
- key: index.html
path: index.html
- name: opentofu-auth
secret:
secretName: opentofu-admin
items:
- key: auth
path: auth
---
apiVersion: v1
kind: Service
metadata:
name: opentofu
spec:
selector:
app: opentofu
ports:
- name: http
port: 8080
targetPort: http

View File

@ -5,6 +5,7 @@ resources:
- garage-configmap.yaml
- garage-statefulset.yaml
- garage-service.yaml
- prole-db-barman-objectstore.yaml
- prole-db.yaml
- prole-db-postgres-service.yaml
- prole-configmap.yaml

View File

@ -0,0 +1,23 @@
apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
name: prole-db-barman-objectstore
spec:
retentionPolicy: 30d
configuration:
destinationPath: s3://prole-db-backups/
endpointURL: http://garage:3900
s3Credentials:
accessKeyId:
name: prole-db-barman-s3
key: ACCESS_KEY_ID
secretAccessKey:
name: prole-db-barman-s3
key: SECRET_ACCESS_KEY
region:
name: prole-db-barman-s3
key: REGION
wal:
compression: gzip
data:
compression: gzip

View File

@ -47,8 +47,14 @@ spec:
walStorage:
size: 1Gi
plugins:
- name: barman-cloud.cloudnative-pg.io
isWALArchiver: true
parameters:
barmanObjectName: prole-db-barman-objectstore
monitoring:
enablePodMonitor: true
enablePodMonitor: false
# managed rw service
managed:

View File

@ -66,29 +66,14 @@ spec:
# volumeMode: Filesystem
walStorage:
size: 1Gi
backup:
barmanObjectStore:
destinationPath: s3://prole-db-backups/
endpointURL: http://garage:3900
s3Credentials:
accessKeyId:
name: prole-db-barman-s3
key: ACCESS_KEY_ID
secretAccessKey:
name: prole-db-barman-s3
key: SECRET_ACCESS_KEY
region:
name: prole-db-barman-s3
key: REGION
wal:
compression: gzip
data:
compression: gzip
retentionPolicy: 30d
plugins:
- name: barman-cloud.cloudnative-pg.io
isWALArchiver: true
parameters:
barmanObjectName: prole-db-barman-objectstore
monitoring:
enablePodMonitor: true
enablePodMonitor: false
# managed rw service
managed:

View File

@ -128,30 +128,23 @@ RUN apt-get remove -y --purge --autoremove build-essential python3-dev && rm -rf
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
COPY prole-db-entrypoint.sh /usr/local/bin/prole-entrypoint.sh
COPY 10_pg_tde_openbao.sh /docker-entrypoint-initdb.d/10_pg_tde_openbao.sh
COPY prole-db-ssh-openbao.sh /usr/local/bin/prole-db-ssh-openbao.sh
# staged copies of the pgdata/*.conf files
COPY postgresql/*.conf /etc/postgresql/17/main/
RUN chown postgres:postgres /etc/postgresql/17/main/*.conf
# 7) Create new user and setup SSH authorized keys
# 7) Create new user
ARG PROLE_USER=prole
ARG PROLE_SSH_PUB_KEY=""
RUN set -eux; \
if [ -n "$PROLE_USER" ] && [ "$PROLE_USER" != "root" ] && [ "$PROLE_USER" != "postgres" ]; then \
if ! id -u "$PROLE_USER" >/dev/null 2>&1; then \
useradd -m -s /bin/bash "$PROLE_USER"; \
fi; \
if [ -n "$PROLE_SSH_PUB_KEY" ]; then \
mkdir -p "/home/$PROLE_USER/.ssh"; \
echo "$PROLE_SSH_PUB_KEY" >> "/home/$PROLE_USER/.ssh/authorized_keys"; \
chown -R "$PROLE_USER:$PROLE_USER" "/home/$PROLE_USER/.ssh"; \
chmod 700 "/home/$PROLE_USER/.ssh"; \
chmod 600 "/home/$PROLE_USER/.ssh/authorized_keys"; \
fi; \
fi
RUN set -eux; \
chmod +x /usr/local/bin/docker-entrypoint.sh /usr/local/bin/prole-entrypoint.sh /docker-entrypoint-initdb.d/10_pg_tde_openbao.sh
chmod +x /usr/local/bin/docker-entrypoint.sh /usr/local/bin/prole-entrypoint.sh /usr/local/bin/prole-db-ssh-openbao.sh /docker-entrypoint-initdb.d/10_pg_tde_openbao.sh
EXPOSE 5432
CMD ["postgres"]

View File

@ -1,6 +1,10 @@
#!/usr/bin/env bash
set -Eeuo pipefail
if [[ -x /usr/local/bin/prole-db-ssh-openbao.sh ]]; then
/usr/local/bin/prole-db-ssh-openbao.sh || true
fi
# If user runs the default "postgres" command, optionally force pg_tde preload.
if [[ "${1:-}" == "postgres" ]] && [[ "${ENABLE_PG_TDE:-}" == "1" ]]; then
# Only inject if user didn't already set shared_preload_libraries explicitly
@ -10,4 +14,4 @@ if [[ "${1:-}" == "postgres" ]] && [[ "${ENABLE_PG_TDE:-}" == "1" ]]; then
fi
# Delegate everything else to the official entrypoint
exec /usr/local/bin/docker-entrypoint.sh "$@"
exec /usr/local/bin/docker-entrypoint.sh "$@"

View File

@ -0,0 +1,91 @@
#!/usr/bin/env bash
set -Eeuo pipefail
PROLE_USER="${PROLE_USER:-prole}"
TOKEN="${OPENBAO_TOKEN:-}"
TOKEN_FILE="${OPENBAO_TOKEN_FILE:-/run/secrets/openbao_token}"
if [[ -z "$TOKEN" && -f "$TOKEN_FILE" ]]; then
TOKEN=$(cat "$TOKEN_FILE")
fi
if [[ -z "$TOKEN" ]]; then
exit 0
fi
NAMESPACE="${NAMESPACE:-}"
if [[ -z "$NAMESPACE" && -f /var/run/secrets/kubernetes.io/serviceaccount/namespace ]]; then
NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
fi
OPENBAO_ADDR="${OPENBAO_ADDR:-${PROLE_OPENBAO_URL:-}}"
if [[ -z "$OPENBAO_ADDR" && -n "$NAMESPACE" ]]; then
OPENBAO_ADDR="http://openbao.${NAMESPACE}.svc.cluster.local:8200"
fi
if [[ -z "$OPENBAO_ADDR" ]]; then
exit 0
fi
OPENBAO_SSH_MOUNT="${OPENBAO_SSH_MOUNT:-kv}"
OPENBAO_SSH_PATH="${OPENBAO_SSH_PATH:-prole/${NAMESPACE}/admin}"
if [[ -z "$OPENBAO_SSH_PATH" || "$OPENBAO_SSH_PATH" == "prole//admin" ]]; then
exit 0
fi
response=$(curl -fsS -H "X-Vault-Token: $TOKEN" \
"$OPENBAO_ADDR/v1/${OPENBAO_SSH_MOUNT}/data/${OPENBAO_SSH_PATH}" 2>/dev/null || true)
if [[ -z "$response" ]]; then
exit 0
fi
ssh_key=$(python3 - <<'PY' <<<"$response"
import base64
import json
import sys
try:
payload = json.load(sys.stdin)
except Exception:
sys.exit(0)
data = payload.get("data", {}).get("data", {})
key_b64 = data.get("admin_public_key_b64")
if key_b64:
try:
print(base64.b64decode(key_b64).decode().strip())
sys.exit(0)
except Exception:
pass
for k in ("admin_public_key", "ssh_public_key", "public_key"):
val = data.get(k)
if val:
print(str(val).strip())
break
PY
)
if [[ -z "$ssh_key" ]]; then
exit 0
fi
if ! id -u "$PROLE_USER" >/dev/null 2>&1; then
exit 0
fi
ssh_dir="/home/$PROLE_USER/.ssh"
auth_keys="${ssh_dir}/authorized_keys"
mkdir -p "$ssh_dir"
touch "$auth_keys"
if ! grep -qxF "$ssh_key" "$auth_keys"; then
printf '%s\n' "$ssh_key" >> "$auth_keys"
fi
chown -R "$PROLE_USER:$PROLE_USER" "$ssh_dir"
chmod 700 "$ssh_dir"
chmod 600 "$auth_keys"

View File

@ -10,7 +10,7 @@ If you wish to deploy Supabase, you can use the `./supabase/deploy.sh` script wh
## Postgres connectivity
Supabase connects to the CNPG primary via the service `prole-db-postgres` on port 5432. The service selector targets the CNPG primary pod:
Supabase connects to the CNPG primary via the service `prole-db-rw` on port 5432. This service is managed by CNPG and always routes to the primary pod (selectors are CNPG-managed, e.g.):
- `cnpg.io/cluster: prole-db`
- `role: primary`
@ -51,7 +51,7 @@ kubectl apply -k k8s/prole
```bash
kubectl get deploy supabase
kubectl get svc supabase
kubectl get svc prole-db-postgres
kubectl get svc prole-db-rw
```
5) Check Supabase logs if needed:
@ -68,7 +68,7 @@ If you need Supabase to connect to a different CNPG cluster name or namespace, u
- If Supabase cannot connect to Postgres, confirm:
- The `prole-db` cluster is healthy and a primary is elected.
- The `prole-db-postgres` service resolves to the primary pod.
- The `prole-db-rw` service resolves to the primary pod.
- The `prole-db-user` secret exists and contains valid credentials.
- If HTTPS does not respond:
@ -77,7 +77,7 @@ If you need Supabase to connect to a different CNPG cluster name or namespace, u
## Files referenced
- `prole-db-postgres-service.yaml`
- `prole-db-postgres-service.yaml` (optional legacy service; Supabase uses `prole-db-rw`)
- `k8s/prole/kustomization.yaml`
- `install.py` (optional feature toggle persistence)
- `./supabase/deploy.sh`

View File

@ -0,0 +1,59 @@
---
- name: Validate k3s API endpoint reachability
hosts: k3s_hosts
gather_facts: false
become: false
vars:
k3s_default_port: 6443
tasks:
- name: Select k3s init host
ansible.builtin.set_fact:
k3s_init_host: "{{ item }}"
when: hostvars[item].k3s_cluster_init | default(false) | bool
loop: "{{ groups['k3s_hosts'] }}"
run_once: true
- name: Select k3s server URL (if provided)
ansible.builtin.set_fact:
k3s_api_url: "{{ hostvars[item].k3s_server_url }}"
when:
- hostvars[item].k3s_server_url is defined
- hostvars[item].k3s_server_url | length > 0
loop: "{{ groups['k3s_hosts'] }}"
run_once: true
- name: Fallback to init host URL
ansible.builtin.set_fact:
k3s_api_url: "https://{{ k3s_init_host | default(groups['k3s_hosts'][0]) }}:{{ k3s_default_port }}"
when: k3s_api_url is not defined or k3s_api_url | length == 0
run_once: true
- name: Extract k3s API host
ansible.builtin.set_fact:
k3s_api_host: "{{ k3s_api_url | regex_replace('^https?://', '') | regex_replace(':.*$', '') }}"
run_once: true
- name: Check TCP port 6443
ansible.builtin.wait_for:
host: "{{ k3s_api_host }}"
port: "{{ k3s_default_port }}"
timeout: 10
delegate_to: localhost
become: false
run_once: true
- name: Check /readyz endpoint
ansible.builtin.uri:
url: "{{ k3s_api_url }}/readyz"
method: GET
status_code: [200, 401, 403]
validate_certs: false
timeout: 5
delegate_to: localhost
become: false
run_once: true
- name: Report k3s API endpoint
ansible.builtin.debug:
msg: "k3s API reachable at {{ k3s_api_url }}"
run_once: true

View File

@ -7,26 +7,12 @@ if [[ -f "$PROJECT_ROOT/env.sh" ]]; then
# shellcheck disable=SC1090
source "$PROJECT_ROOT/env.sh"
fi
DEV_HOME="${DEV_HOME:-$HOME/dev}"
DEV_HOME="${DEV_HOME/#\~/$HOME}"
SUPABASE_DIR="$DEV_HOME/supabase"
DOCKER_DIR="$SUPABASE_DIR/docker"
COMPOSE_FILE="$DOCKER_DIR/docker-compose.yml"
DEV_COMPOSE_FILE="$DOCKER_DIR/dev/docker-compose.dev.yml"
ENV_EXAMPLE="$DOCKER_DIR/.env.example"
ENV_FILE="$DOCKER_DIR/.env"
SUPABASE_K8S_DIR="${SUPABASE_K8S_DIR:-$SCRIPT_DIR/../build/supabase-k8s}"
DOCKER_IMPORT_DIR="${DOCKER_IMPORT_DIR:-$PROJECT_ROOT/data/docker-import}"
SUPABASE_IMAGE_PLATFORM="${SUPABASE_IMAGE_PLATFORM:-}"
SUPABASE_IMAGE_PLATFORMS="${SUPABASE_IMAGE_PLATFORMS:-linux/amd64 linux/arm64}"
SUPABASE_POSTGRES_PORT="${SUPABASE_POSTGRES_PORT:-15432}"
PROLE_DB_SERVICE="${PROLE_DB_SERVICE:-prole-db-postgres}"
PROLE_DB_NAMESPACE="${PROLE_DB_NAMESPACE:-}"
MODE="k3d"
USE_DEV_HELPERS="false"
FOREGROUND="false"
FORCE="true"
PROLE_CFG_PATH=""
usage() {
cat <<'USAGE'
@ -40,11 +26,12 @@ Usage:
./deploy.sh [options]
Options:
--mode <local|k3d> Deployment mode ('local' for Docker Compose, 'k3d' for Kubernetes; default: k3d)
--mode <local|k3d|k8s> Deployment mode ('local' for Docker Compose, 'k3d' for local k3d, 'k8s' for generic k8s/k3s; default: k3d)
--with-dev-helpers Include docker/dev/docker-compose.dev.yml
--foreground Run docker compose in the foreground (default: detached, local mode only)
-f, --force Reset the Supabase namespace before applying manifests (k3d only; default)
--no-force Skip namespace reset (k3d only)
-c, --config <path> Path to prole.cfg (loads environment defaults)
-h, --help Show help
Notes:
@ -72,6 +59,49 @@ log() {
echo "==> $*"
}
apply_defaults() {
DEV_HOME="${DEV_HOME:-$HOME/dev}"
DEV_HOME="${DEV_HOME/#\~/$HOME}"
SUPABASE_DIR="$DEV_HOME/supabase"
DOCKER_DIR="$SUPABASE_DIR/docker"
COMPOSE_FILE="$DOCKER_DIR/docker-compose.yml"
DEV_COMPOSE_FILE="$DOCKER_DIR/dev/docker-compose.dev.yml"
ENV_EXAMPLE="$DOCKER_DIR/.env.example"
ENV_FILE="$DOCKER_DIR/.env"
SUPABASE_K8S_DIR="${SUPABASE_K8S_DIR:-$SCRIPT_DIR/../build/supabase-k8s}"
DOCKER_IMPORT_DIR="${DOCKER_IMPORT_DIR:-$PROJECT_ROOT/data/docker-import}"
SUPABASE_IMAGE_PLATFORM="${SUPABASE_IMAGE_PLATFORM:-}"
SUPABASE_IMAGE_PLATFORMS="${SUPABASE_IMAGE_PLATFORMS:-linux/amd64 linux/arm64}"
SUPABASE_POSTGRES_PORT="${SUPABASE_POSTGRES_PORT:-15432}"
PROLE_DB_SERVICE="${PROLE_DB_SERVICE:-prole-db-rw}"
PROLE_DB_NAMESPACE="${PROLE_DB_NAMESPACE:-}"
}
load_prole_cfg() {
local cfg_loader="$PROJECT_ROOT/etc/prole_cfg.sh"
local cfg_path="${PROLE_CFG_PATH:-}"
if [[ -z "$cfg_path" ]]; then
return 0
fi
if [[ -n "$cfg_path" ]]; then
if [[ -d "$cfg_path" ]]; then
cfg_path="$cfg_path/prole.cfg"
fi
[[ -f "$cfg_path" ]] || die "Config file not found: $cfg_path"
PROLE_CONF="$(cd "$(dirname "$cfg_path")" && pwd)"
export PROLE_CONF
fi
if [[ -f "$cfg_loader" ]]; then
# shellcheck disable=SC1090
source "$cfg_loader"
else
warn "prole_cfg.sh not found; config defaults may be incomplete."
fi
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
}
@ -115,12 +145,21 @@ ensure_kompose() {
}
list_k3d_clusters() {
if k3d cluster list -o json >/dev/null 2>&1; then
k3d cluster list -o json | python - <<'PY'
local json=""
if json="$(k3d cluster list -o json 2>/dev/null)"; then
if [[ -n "$json" ]]; then
if printf '%s' "$json" | python - <<'PY'
import json
import sys
data = json.load(sys.stdin)
raw = sys.stdin.read()
if not raw.strip():
sys.exit(1)
try:
data = json.loads(raw)
except Exception:
sys.exit(1)
items = data.get("items") if isinstance(data, dict) else data
if not items:
sys.exit(0)
@ -129,7 +168,10 @@ for item in items:
if name:
print(name)
PY
return 0
then
return 0
fi
fi
fi
k3d cluster list 2>/dev/null | awk 'NR>1 {print $1}'
@ -182,6 +224,16 @@ parse_args() {
MODE="${2:-}"
shift 2
;;
-c|--config)
PROLE_CFG_PATH="${2:-}"
[[ -n "$PROLE_CFG_PATH" ]] || die "Missing value for $1"
shift 2
;;
--config=*)
PROLE_CFG_PATH="${1#*=}"
[[ -n "$PROLE_CFG_PATH" ]] || die "Missing value for $1"
shift
;;
--with-dev-helpers)
USE_DEV_HELPERS="true"
shift
@ -318,6 +370,20 @@ ensure_image_artifact() {
return 0
fi
mkdir -p "$DOCKER_IMPORT_DIR"
# Prefer buildx export to reliably produce single-arch tarballs with containerd-backed Docker
if docker buildx version >/dev/null 2>&1; then
log "Fetching image $img for platform $platform (buildx export)"
if ! printf 'FROM %s\n' "$img" \
| docker buildx build --pull --platform "$platform" -t "$platform_tag" \
--output "type=docker,dest=$tar_path" - >/dev/null 2>&1; then
die "Failed to export image $img ($platform) via buildx"
fi
return 0
fi
# Fallback: traditional pull + save (may fail on some Docker/containerd combos)
log "Fetching image $img for platform $platform"
if ! docker pull --platform "$platform" "$img" >/dev/null 2>&1; then
if docker image inspect "$img" >/dev/null 2>&1; then
@ -327,10 +393,11 @@ ensure_image_artifact() {
fi
fi
docker tag "$img" "$platform_tag"
mkdir -p "$DOCKER_IMPORT_DIR"
docker tag "$img" "$platform_tag" >/dev/null 2>&1 || true
log "Saving $img ($platform) to $tar_path"
docker save -o "$tar_path" "$platform_tag" >/dev/null
if ! docker save -o "$tar_path" "$platform_tag" >/dev/null 2>&1; then
die "Failed to save image $img ($platform) to $tar_path"
fi
}
load_image_for_platform() {
@ -411,14 +478,12 @@ prefetch_k3d_images() {
load_image_for_platform "$img" "$deploy_platform"
done
log "Importing images into k3d"
for img in $images; do
if [[ -n "${K3D_CLUSTER_NAME:-}" ]]; then
k3d image import "$img" -c "$K3D_CLUSTER_NAME"
else
k3d image import "$img"
fi
done
# shellcheck disable=SC2086
if [[ -n "${K3D_CLUSTER_NAME:-}" ]]; then
k3d image import $images -c "$K3D_CLUSTER_NAME"
else
k3d image import $images
fi
}
resolve_prole_db_namespace() {
@ -426,6 +491,11 @@ resolve_prole_db_namespace() {
return 0
fi
if [[ -n "${NAMESPACE:-}" ]]; then
PROLE_DB_NAMESPACE="$NAMESPACE"
return 0
fi
if kubectl get namespace prole >/dev/null 2>&1; then
PROLE_DB_NAMESPACE="prole"
else
@ -522,6 +592,40 @@ generate_k8s_manifests() {
patch_db_deployment_port
write_supabase_postgres_service
write_db_alias_service
sanitize_container_names
}
sanitize_container_names() {
log "Sanitizing container names in manifests..."
local files
files=("$SUPABASE_K8S_DIR"/*-deployment.yaml)
for file in "${files[@]}"; do
[[ -f "$file" ]] || continue
# Replace dots with hyphens in container names (spec.template.spec.containers[].name)
python - "$file" <<'PY'
import sys
import re
path = sys.argv[1]
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
def replace_dots(match):
prefix = match.group(1)
name = match.group(2)
if '.' in name:
return prefix + name.replace('.', '-')
return match.group(0)
# Match 'name: some.container.name' with at least 4 spaces of indentation
# This helps avoid metadata.name which usually has 2 spaces.
new_content = re.sub(r'(\s{4,}name:\s+)([^\n]+)', replace_dots, content)
if new_content != content:
with open(path, 'w', encoding='utf-8') as f:
f.write(new_content)
PY
done
}
ensure_supabase_namespace() {
@ -541,11 +645,18 @@ force_reset_supabase_namespace() {
python - <<'PY' | kubectl replace --raw "/api/v1/namespaces/supabase/finalize" -f - >/dev/null 2>&1 || true
import json
import subprocess
import sys
raw = subprocess.check_output(["kubectl", "get", "namespace", "supabase", "-o", "json"])
data = json.loads(raw)
data["spec"]["finalizers"] = []
print(json.dumps(data))
try:
raw = subprocess.check_output(["kubectl", "get", "namespace", "supabase", "-o", "json"])
if not raw:
sys.exit(0)
data = json.loads(raw)
if "spec" in data:
data["spec"]["finalizers"] = []
print(json.dumps(data))
except Exception:
sys.exit(0)
PY
kubectl wait --for=delete namespace/supabase --timeout=120s >/dev/null 2>&1 || true
fi
@ -592,8 +703,45 @@ run_k3d() {
log "Access Studio via Kong proxy (check ingress/service in 'supabase' namespace)."
}
run_k8s() {
ensure_repo
log "Prole::Supabase k8s deploy (Kubernetes - generic/containerd)"
log "Repo: $SUPABASE_DIR"
require_cmd kubectl
kubectl cluster-info >/dev/null 2>&1 || die "Kubernetes cluster not reachable"
if [[ "$FORCE" == "true" ]]; then
force_reset_supabase_namespace
else
ensure_supabase_namespace
fi
export SUPABASE_HOME="$SUPABASE_DIR"
if [[ "$USE_DEV_HELPERS" == "true" ]]; then
export SUPABASE_USE_DEV_COMPOSE=1
fi
# For generic k8s/k3s clusters (containerd), let nodes pull appropriate arch images directly
# Optionally, a future enhancement could push pre-fetched images to an internal registry.
generate_k8s_manifests
if [[ -f "$SUPABASE_K8S_DIR/supabase-namespace.yaml" ]]; then
kubectl apply -f "$SUPABASE_K8S_DIR/supabase-namespace.yaml"
fi
log "Applying Supabase manifests from $SUPABASE_K8S_DIR"
kubectl apply -f "$SUPABASE_K8S_DIR"
log "Deployment complete."
log "Access Studio via Kong proxy (check ingress/service in 'supabase' namespace)."
}
main() {
parse_args "$@"
load_prole_cfg
apply_defaults
if [[ -z "$MODE" ]]; then
usage
@ -607,6 +755,9 @@ main() {
k3d)
run_k3d
;;
k8s)
run_k8s
;;
*)
die "Unsupported mode: $MODE"
;;