feat(infra): use beanflows_service for supervisor

- materia-supervisor.service: User=root → User=beanflows_service,
  add PATH so uv (~/.local/bin) is found without a login shell
- setup_server.sh: full rewrite — creates beanflows_service (nologin),
  generates SSH deploy key + age keypair as service user at XDG path
  (~/.config/sops/age/keys.txt), installs age/sops/rclone as root,
  prints both public keys + numbered next-step instructions
- bootstrap_supervisor.sh: full rewrite — removes GITLAB_READ_TOKEN
  requirement, clones via SSH as service user, installs uv as service
  user, decrypts with SOPS auto-discovery, uv sync as service user,
  systemctl as root
- web/deploy.sh: remove self-contained sops/age install + keypair
  generation; replace with simple sops check (exit if missing) and
  SOPS auto-discovery decrypt (no explicit key file needed)
- infra/readme.md: update architecture diagram for beanflows_service
  paths, update setup steps to match new scripts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-26 21:33:31 +01:00
parent b27f06d811
commit 0317cb885f
5 changed files with 227 additions and 185 deletions

View File

@@ -6,129 +6,123 @@
# ssh root@<server_ip> 'bash -s' < infra/bootstrap_supervisor.sh
#
# Prerequisites:
# - age keypair exists at /opt/materia/age-key.txt
# (or SOPS_AGE_KEY_FILE env var pointing elsewhere)
# - The server age public key is already in .sops.yaml and .env.prod.sops
# (run setup_server.sh first, then add the key and re-commit)
# - GITLAB_READ_TOKEN is set (GitLab project access token, read-only)
# - setup_server.sh already run (beanflows_service user, SSH deploy key, age keypair)
# - Deploy key added to GitLab (Settings → Repository → Deploy Keys)
# - Server age public key added to .sops.yaml + .env.prod.sops committed + pushed
set -euo pipefail
echo "=== Materia Supervisor Bootstrap ==="
echo "This script will:"
echo " 1. Install dependencies (git, uv, sops, age)"
echo " 2. Clone the materia repository"
echo " 3. Decrypt secrets from .env.prod.sops"
echo " 4. Set up systemd service"
echo " 5. Start the supervisor"
echo ""
if [ "$EUID" -ne 0 ]; then
echo "ERROR: This script must be run as root"
exit 1
fi
# ── Configuration ──────────────────────────────────────────
SERVICE_USER="beanflows_service"
REPO_DIR="/opt/materia"
GITLAB_PROJECT="deemanone/materia"
AGE_KEY_FILE="${SOPS_AGE_KEY_FILE:-$REPO_DIR/age-key.txt}"
UV="/home/${SERVICE_USER}/.local/bin/uv"
if [ -z "${GITLAB_READ_TOKEN:-}" ]; then
echo "ERROR: GITLAB_READ_TOKEN not set"
echo " export GITLAB_READ_TOKEN=<gitlab-project-access-token>"
exit 1
fi
echo "=== Materia Supervisor Bootstrap ==="
echo ""
REPO_URL="https://oauth2:${GITLAB_READ_TOKEN}@gitlab.com/${GITLAB_PROJECT}.git"
[ "$(id -u)" = "0" ] || { echo "ERROR: Run as root"; exit 1; }
# ── System dependencies ────────────────────────────────────────────────────────
# ── System dependencies ────────────────────────────────────
echo "--- Installing system dependencies ---"
apt-get update -q
apt-get install -y -q git curl ca-certificates
# ── uv ─────────────────────────────────────────────────────
# ── uv (installed as service user) ────────────────────────────────────────────
echo "--- Installing uv ---"
if ! command -v uv &>/dev/null; then
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
echo 'export PATH="$HOME/.local/bin:$PATH"' >> /root/.bashrc
if [ ! -f "${UV}" ]; then
sudo -u "${SERVICE_USER}" bash -c \
'curl -LsSf https://astral.sh/uv/install.sh | sh'
fi
# ── sops + age ─────────────────────────────────────────────
# ── sops + age (as root, idempotent — setup_server.sh may have done this) ─────
echo "--- Installing sops + age ---"
ARCH=$(uname -m)
case "$ARCH" in
x86_64) ARCH_SOPS="amd64"; ARCH_AGE="amd64" ;;
aarch64) ARCH_SOPS="arm64"; ARCH_AGE="arm64" ;;
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
case "${ARCH}" in
x86_64) ARCH_TAG="amd64" ;;
aarch64) ARCH_TAG="arm64" ;;
*) echo "Unsupported architecture: ${ARCH}"; exit 1 ;;
esac
if ! command -v age &>/dev/null; then
if ! command -v age >/dev/null 2>&1; then
AGE_VERSION="v1.3.1"
curl -fsSL "https://dl.filippo.io/age/${AGE_VERSION}?for=linux/${AGE_ARCH}" -o /tmp/age.tar.gz
curl -fsSL "https://dl.filippo.io/age/${AGE_VERSION}?for=linux/${ARCH_TAG}" -o /tmp/age.tar.gz
tar -xzf /tmp/age.tar.gz -C /usr/local/bin --strip-components=1 age/age age/age-keygen
chmod +x /usr/local/bin/age /usr/local/bin/age-keygen
rm /tmp/age.tar.gz
fi
if ! command -v sops &>/dev/null; then
if ! command -v sops >/dev/null 2>&1; then
SOPS_VERSION="v3.12.1"
curl -fsSL "https://github.com/getsops/sops/releases/download/${SOPS_VERSION}/sops-${SOPS_VERSION}.linux.${ARCH_SOPS}" -o /usr/local/bin/sops
curl -fsSL \
"https://github.com/getsops/sops/releases/download/${SOPS_VERSION}/sops-${SOPS_VERSION}.linux.${ARCH_TAG}" \
-o /usr/local/bin/sops
chmod +x /usr/local/bin/sops
fi
# ── Clone repository ───────────────────────────────────────
# ── Clone repository via SSH as service user ──────────────────────────────────
echo "--- Cloning repository ---"
if [ -d "$REPO_DIR/.git" ]; then
if [ -d "${REPO_DIR}/.git" ]; then
echo "Repository already exists — fetching latest tags..."
cd "$REPO_DIR"
git fetch --tags --prune-tags origin
sudo -u "${SERVICE_USER}" git -C "${REPO_DIR}" fetch --tags --prune-tags origin
else
git clone "$REPO_URL" "$REPO_DIR"
cd "$REPO_DIR"
sudo -u "${SERVICE_USER}" git clone \
"git@gitlab.com:${GITLAB_PROJECT}.git" "${REPO_DIR}"
fi
# Checkout latest release tag (same logic as supervisor)
LATEST_TAG=$(git tag --list --sort=-version:refname "v*" | head -1)
if [ -n "$LATEST_TAG" ]; then
echo "Checking out $LATEST_TAG..."
git checkout --detach "$LATEST_TAG"
LATEST_TAG=$(sudo -u "${SERVICE_USER}" \
git -C "${REPO_DIR}" tag --list --sort=-version:refname "v*" | head -1)
if [ -n "${LATEST_TAG}" ]; then
echo "Checking out ${LATEST_TAG}..."
sudo -u "${SERVICE_USER}" git -C "${REPO_DIR}" checkout --detach "${LATEST_TAG}"
else
echo "No release tags found — staying on current HEAD"
fi
# ── Check age keypair ──────────────────────────────────────
# ── Check age keypair ─────────────────────────────────────────────────────────
echo "--- Checking age keypair ---"
if [ ! -f "$AGE_KEY_FILE" ]; then
echo "ERROR: Age keypair not found at $AGE_KEY_FILE"
AGE_KEY_FILE="/home/${SERVICE_USER}/.config/sops/age/keys.txt"
if [ ! -f "${AGE_KEY_FILE}" ]; then
echo ""
echo "Run infra/setup_server.sh first to generate the keypair, then:"
echo " 1. Copy the public key from setup_server.sh output"
echo " 2. Add it to .sops.yaml on your workstation"
echo " 3. Run: sops updatekeys .env.prod.sops"
echo " 4. Commit + push and re-run this bootstrap"
echo "ERROR: Age keypair not found at ${AGE_KEY_FILE}"
echo ""
echo "Run infra/setup_server.sh first, then:"
echo " 1. Add the SSH deploy key to GitLab (Settings → Repository → Deploy Keys)"
echo " 2. Add the age public key to .sops.yaml on your workstation"
echo " 3. Run: sops updatekeys .env.prod.sops && git push"
echo " 4. Re-run this bootstrap"
exit 1
fi
export SOPS_AGE_KEY_FILE="$AGE_KEY_FILE"
# ── Decrypt secrets ────────────────────────────────────────
# ── Decrypt secrets (as service user — SOPS auto-discovers age key from XDG) ──
echo "--- Decrypting secrets from .env.prod.sops ---"
sops --input-type dotenv --output-type dotenv -d "$REPO_DIR/.env.prod.sops" > "$REPO_DIR/.env"
chmod 600 "$REPO_DIR/.env"
echo "Secrets written to $REPO_DIR/.env"
sudo -u "${SERVICE_USER}" bash -c \
"sops --input-type dotenv --output-type dotenv -d ${REPO_DIR}/.env.prod.sops > ${REPO_DIR}/.env"
chmod 600 "${REPO_DIR}/.env"
echo "Secrets written to ${REPO_DIR}/.env"
# ── Data directories ───────────────────────────────────────────────────────────
# ── Data directories ───────────────────────────────────────
echo "--- Creating data directories ---"
mkdir -p /data/materia/landing
chown -R "${SERVICE_USER}:${SERVICE_USER}" /data/materia
# ── Python dependencies (as service user) ─────────────────────────────────────
# ── Python dependencies ────────────────────────────────────
echo "--- Installing Python dependencies ---"
cd "$REPO_DIR"
uv sync --all-packages
sudo -u "${SERVICE_USER}" bash -c "cd ${REPO_DIR} && ${UV} sync --all-packages"
# ── Systemd service (as root) ──────────────────────────────────────────────────
# ── Systemd service ────────────────────────────────────────
echo "--- Setting up systemd service ---"
cp "$REPO_DIR/infra/supervisor/materia-supervisor.service" /etc/systemd/system/materia-supervisor.service
cp "${REPO_DIR}/infra/supervisor/materia-supervisor.service" \
/etc/systemd/system/materia-supervisor.service
echo "--- Enabling and starting service ---"
systemctl daemon-reload
@@ -138,9 +132,9 @@ systemctl restart materia-supervisor
echo ""
echo "=== Bootstrap complete! ==="
echo ""
echo "Check status: systemctl status materia-supervisor"
echo "View logs: journalctl -u materia-supervisor -f"
echo "Workflow status: cd $REPO_DIR && uv run python src/materia/supervisor.py status"
echo "Check status: systemctl status materia-supervisor"
echo "View logs: journalctl -u materia-supervisor -f"
echo "Workflow status: sudo -u ${SERVICE_USER} ${UV} run -p ${REPO_DIR} python src/materia/supervisor.py status"
echo ""
echo "Repo: $REPO_DIR"
echo "Tag: $(cd $REPO_DIR && git describe --tags --always)"
echo "Repo: ${REPO_DIR}"
echo "Tag: $(sudo -u "${SERVICE_USER}" git -C "${REPO_DIR}" describe --tags --always)"