#!/usr/bin/env bash # Install/update backup-manager (cluster mode + on-host runners). # # Operating model: # 1. Receives bootstrap secrets via env (decrypted by the wrapper from # SOPS+Age — only at deploy time; runtime keys live in vault per # ADR-017). # 2. Creates the bootstrap K8s Secret in $NAMESPACE. # 3. Renders and applies Deployment + Service + ServiceMonitor + # PrometheusRule for the daemon (cluster mode). # 4. For every entry in system-backups.ncl: SSH to the selected hosts and # install the prvng-backup binary + systemd timer or cron.d entry. # # Required env (typically populated by `provisioning component install …`): # VAULT_BOOTSTRAP_TOKEN # NATS_BOOTSTRAP_NKEY_SEED # BACKUP_AGE_KEY bootstrap age key (NOT in vault — ADR-011) # BACKUP_S3_PRIMARY_ACCESS_KEY # BACKUP_S3_PRIMARY_SECRET_KEY # BACKUP_B2_REPLICA_KEY_ID # BACKUP_B2_REPLICA_APPLICATION_KEY # NAMESPACE (default: backup-system) # COMPONENT_PATH (path to instance .ncl rendered to JSON for Jinja2) set -euo pipefail NAMESPACE="${NAMESPACE:-backup-system}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TEMPLATES="$SCRIPT_DIR/templates" # ── Pre-flight ────────────────────────────────────────────────────────────── require_var() { local name="$1" if [ -z "${!name:-}" ]; then echo "ERROR: required env var $name is not set" >&2 exit 1 fi } require_var VAULT_BOOTSTRAP_TOKEN require_var NATS_BOOTSTRAP_NKEY_SEED require_var BACKUP_AGE_KEY require_var BACKUP_S3_PRIMARY_ACCESS_KEY require_var BACKUP_S3_PRIMARY_SECRET_KEY require_var BACKUP_B2_REPLICA_KEY_ID require_var BACKUP_B2_REPLICA_APPLICATION_KEY command -v kubectl >/dev/null || { echo "kubectl is required" >&2; exit 1; } command -v jq >/dev/null || { echo "jq is required" >&2; exit 1; } # ── Namespace ──────────────────────────────────────────────────────────────── kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - # ── Bootstrap Secret ───────────────────────────────────────────────────────── # Pattern mirrors zot S3 install (provisioning/extensions/components/zot/cluster/install-zot.sh): # wrapper passes secrets as env vars; we materialise them as a K8s Secret. # Daemon reads on first run and promotes runtime credentials into vault. kubectl create secret generic backup-manager-bootstrap \ --namespace="$NAMESPACE" \ --from-literal=vault_token="$VAULT_BOOTSTRAP_TOKEN" \ --from-literal=nats_nkey_seed="$NATS_BOOTSTRAP_NKEY_SEED" \ --from-literal=age_key="$BACKUP_AGE_KEY" \ --from-literal=s3_primary_access_key="$BACKUP_S3_PRIMARY_ACCESS_KEY" \ --from-literal=s3_primary_secret_key="$BACKUP_S3_PRIMARY_SECRET_KEY" \ --from-literal=b2_replica_key_id="$BACKUP_B2_REPLICA_KEY_ID" \ --from-literal=b2_replica_application_key="$BACKUP_B2_REPLICA_APPLICATION_KEY" \ --dry-run=client -o yaml | kubectl apply -f - # ── Deployment + Service ───────────────────────────────────────────────────── # Templates are Jinja2; rendered by `nu render-template.nu` (provisioning helper) # with the JSON dump of the component config bound as $component. # Here we leave a render hook the wrapper invokes; if invoked directly without # the wrapper, fall back to substituting NAMESPACE and DAEMON_IMAGE only. RENDER_BIN="${RENDER_BIN:-render-template.nu}" RENDER_INPUT="${COMPONENT_PATH:-}" render_template() { local template="$1" if command -v "$RENDER_BIN" >/dev/null && [ -n "$RENDER_INPUT" ]; then "$RENDER_BIN" "$template" --json "$RENDER_INPUT" else # Minimal substitution fallback — sufficient for development. sed -e "s|{{ namespace }}|$NAMESPACE|g" \ -e "s|{{ daemon_image }}|${DAEMON_IMAGE:-ghcr.io/jesusperezlorenzo/backup-manager-runner:0.1.0}|g" \ -e "s|{{ daemon_http_port }}|${DAEMON_HTTP_PORT:-9099}|g" \ -e "s|{{ daemon_metrics_port }}|${DAEMON_METRICS_PORT:-9100}|g" \ "$template" fi } for tpl in deployment.yaml.j2 service.yaml.j2 servicemonitor.yaml.j2; do if [ -f "$TEMPLATES/$tpl" ]; then render_template "$TEMPLATES/$tpl" | kubectl apply -f - fi done # ── PrometheusRule (alerts from prometheus.ncl) ───────────────────────────── # The alert rules are declared in the prometheus component; install-prometheus.sh # is responsible for rendering them. backup-manager only emits the metrics; the # rules live with the observability stack. # ── On-host runners ────────────────────────────────────────────────────────── # For each SystemBackupDef in the workspace, install /usr/local/bin/prvng-backup # and a systemd timer or cron.d entry on the selected hosts. This step is # delegated to the provisioning host runner (Nushell) when available; here we # emit the list to stdout so the caller can act. SYSTEM_BACKUPS_PATH="${SYSTEM_BACKUPS_PATH:-infra/libre-wuji/system-backups.ncl}" if command -v nickel >/dev/null && [ -f "$SYSTEM_BACKUPS_PATH" ]; then echo "==> SystemBackupDef entries requiring on-host runner installation:" nickel export "$SYSTEM_BACKUPS_PATH" 2>/dev/null \ | jq -r 'to_entries[] | " \(.value.name) -> host_selector=\(.value.host_selector.kind), members=\(.value.host_selector.members | join(","))"' || true echo "==> Run 'provisioning component install backup_manager --target-hosts' to deploy binaries." fi echo "==> backup-manager cluster install complete in namespace $NAMESPACE"