diff --git a/create_pod_finance.sh b/create_pod_finance.sh new file mode 100755 index 0000000..dc39cb8 --- /dev/null +++ b/create_pod_finance.sh @@ -0,0 +1,202 @@ +#!/bin/bash + +# To be run by user wlfb to create pod for the Finanzberatungs-Tool +# (API + Postgres + Grafana). + +set -e + +# Environment variables +POD_NAME='finance_pod' +DB_CTR_NAME='finance-db_ctr' +API_CTR_NAME='finance-api_ctr' +GRAFANA_CTR_NAME='finance-grafana_ctr' + +# All image tags are pinned to fixed, immutable versions (no floating tags +# such as :latest, :3 or :16). Verified against the upstream registries on +# 2026-07-17 via `podman pull` (both tags exist as-is; no substitution +# needed). +POSTGRES_IMAGE='docker.io/library/postgres:17.10' +GRAFANA_IMAGE='docker.io/grafana/grafana-oss:12.1.0' +API_IMAGE='localhost/finance-api:0.1.0' + +HOST_LOCAL_IP='127.0.0.1' +# Expose API on 8096 -> 8000 +API_HOST_PORT='8096' +API_CONTAINER_PORT='8000' +# Expose Grafana on 8097 -> 3000 +GRAFANA_HOST_PORT='8097' +GRAFANA_CONTAINER_PORT='3000' +# Postgres is only reachable inside the pod (localhost:5432 from API/Grafana) + +BIND_DIR="$HOME/.local/share/$POD_NAME" +POSTGRES_DATA_DIR="$BIND_DIR/postgres-data" +GRAFANA_DATA_DIR="$BIND_DIR/grafana-data" +INBOX_DIR="$BIND_DIR/inbox" +UPLOADS_DIR="$BIND_DIR/uploads" +USER_SYSTEMD_DIR="$HOME/.config/systemd/user" + +FINANCE_DIR="$HOME/bin/finance" +ENV_FILE="$FINANCE_DIR/.env" +GRAFANA_PROVISIONING_DIR="$FINANCE_DIR/grafana/provisioning" +GRAFANA_DASHBOARDS_DIR="$FINANCE_DIR/grafana/dashboards" + +# --- Secrets bootstrap ------------------------------------------------------- +# On first run, generate all secrets into finance/.env (gitignored) and print +# the GUI login password once. On subsequent runs the existing .env is reused +# unchanged, so re-running this script never rotates secrets under existing +# data. +if [ ! -f "$ENV_FILE" ]; then + GUI_PASSWORD=$(openssl rand -base64 12) + cat > "$ENV_FILE" </dev/null | \ + grep -q "pod-$POD_NAME.service"; then + systemctl --user stop "pod-$POD_NAME.service" || true +fi + +# Prepare directories +mkdir -p "$POSTGRES_DATA_DIR" "$GRAFANA_DATA_DIR" "$INBOX_DIR" "$UPLOADS_DIR" \ + "$USER_SYSTEMD_DIR" + +# The Grafana image runs as a non-root user (uid 472) that belongs to group 0 +# (root). Its data dir is bind-mounted from the host, where it is owned by the +# (rootless) container root. Make the dir group-writable with the setgid bit +# so the non-root Grafana process can write its sqlite db/plugins (same +# "arbitrary uid + gid 0" pattern as example_create_pod_langflow.sh's +# LANGFLOW_DATA_DIR fix). +chmod g+rwxs "$GRAFANA_DATA_DIR" + +# Build the API image (Task 15 Containerfile) +podman build -t "$API_IMAGE" "$FINANCE_DIR" + +# Create pod if not yet existing +if ! podman pod exists "$POD_NAME"; then + podman pod create -n "$POD_NAME" \ + -p "$HOST_LOCAL_IP:$API_HOST_PORT:$API_CONTAINER_PORT" \ + -p "$HOST_LOCAL_IP:$GRAFANA_HOST_PORT:$GRAFANA_CONTAINER_PORT" + echo "Pod '$POD_NAME' created (rc=$?)" +else + echo "Pod '$POD_NAME' already exists." +fi + +# Remove any old containers (ignore errors if they don't exist) +podman rm -f "$DB_CTR_NAME" || true +podman rm -f "$API_CTR_NAME" || true +podman rm -f "$GRAFANA_CTR_NAME" || true + +# Postgres container +podman run -d --name "$DB_CTR_NAME" --pod "$POD_NAME" \ + -e POSTGRES_USER=finance \ + -e POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \ + -e POSTGRES_DB=finance \ + -v "$POSTGRES_DATA_DIR:/var/lib/postgresql/data:Z" \ + "$POSTGRES_IMAGE" +echo "Container '$DB_CTR_NAME' started (rc=$?)" + +# Wait for Postgres to be ready before starting API / Grafana +echo "Waiting for Postgres to be ready (pg_isready)..." +for attempt in $(seq 1 30); do + if podman exec "$DB_CTR_NAME" pg_isready -q >/dev/null 2>&1; then + echo "Postgres is ready." + break + fi + sleep 2 + if [ "$attempt" -eq 30 ]; then + echo "ERROR: Postgres did not become ready in time." >&2 + exit 1 + fi +done + +# Create the read-only role used by the Grafana datasource, idempotently. +echo "Ensuring read-only role 'finance_read' exists..." +podman exec "$DB_CTR_NAME" psql -U finance -d finance -c \ + "DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='finance_read') + THEN CREATE ROLE finance_read LOGIN PASSWORD '$FINANCE_READ_PASSWORD'; END IF; END \$\$; + GRANT CONNECT ON DATABASE finance TO finance_read; + GRANT USAGE ON SCHEMA public TO finance_read; + GRANT SELECT ON ALL TABLES IN SCHEMA public TO finance_read; + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO finance_read;" +echo "Role 'finance_read' is ready." + +# API container (runs alembic upgrade head on start via entrypoint.sh) +podman run -d --name "$API_CTR_NAME" --pod "$POD_NAME" \ + -e FB_DATABASE_URL="postgresql+psycopg://finance:$POSTGRES_PASSWORD@localhost:5432/finance" \ + -e FB_API_KEY \ + -e FB_SESSION_SECRET \ + -e FB_GUI_USER \ + -e FB_GUI_PASSWORD_HASH \ + -e FB_INBOX_DIR=/data/inbox \ + -e FB_UPLOADS_DIR=/data/uploads \ + -v "$INBOX_DIR:/data/inbox:Z" \ + -v "$UPLOADS_DIR:/data/uploads:Z" \ + "$API_IMAGE" +echo "Container '$API_CTR_NAME' started (rc=$?)" + +# Grafana container (provisioning + dashboards mounted read-only; datasource +# yaml interpolates FINANCE_READ_PASSWORD) +podman run -d --name "$GRAFANA_CTR_NAME" --pod "$POD_NAME" \ + -e GF_SECURITY_ADMIN_PASSWORD="$GRAFANA_ADMIN_PASSWORD" \ + -e FINANCE_READ_PASSWORD \ + -v "$GRAFANA_PROVISIONING_DIR:/etc/grafana/provisioning:Z,ro" \ + -v "$GRAFANA_DASHBOARDS_DIR:/var/lib/grafana/dashboards:Z,ro" \ + -v "$GRAFANA_DATA_DIR:/var/lib/grafana:Z" \ + "$GRAFANA_IMAGE" +echo "Container '$GRAFANA_CTR_NAME' started (rc=$?)" + +# Generate systemd service files +cd "$USER_SYSTEMD_DIR" +podman generate systemd --name --new --files "$POD_NAME" +echo "Generated systemd service files (rc=$?)" + +# Stop & remove live pod and containers +podman pod stop --ignore --time 15 "$POD_NAME" +podman pod rm -f --ignore "$POD_NAME" +if podman pod exists "$POD_NAME"; then + echo "ERROR: Pod $POD_NAME still exists." >&2 + exit 1 +else + echo "Stopped & removed live pod $POD_NAME and containers." +fi + +# Enable systemd user services +systemctl --user daemon-reload +# pod service (creates pod + containers) +systemctl --user enable --now "pod-${POD_NAME}.service" +systemctl --user is-enabled "pod-${POD_NAME}.service" +systemctl --user is-active "pod-${POD_NAME}.service" +echo "Enabled systemd service pod-${POD_NAME}.service (rc=$?)" +echo "To view status: systemctl --user status pod-${POD_NAME}.service" +echo "To view logs: journalctl --user -u pod-${POD_NAME}.service -f" + +# Wait for API and Grafana readiness +CHECK_URL_API="http://$HOST_LOCAL_IP:$API_HOST_PORT/login" +CHECK_URL_GRAFANA="http://$HOST_LOCAL_IP:$GRAFANA_HOST_PORT/api/health" +for attempt in $(seq 1 30); do + API_CODE=$(curl -s -o /dev/null -w '%{http_code}' "$CHECK_URL_API" || true) + GRAFANA_CODE=$(curl -s -o /dev/null -w '%{http_code}' "$CHECK_URL_GRAFANA" || true) + if [ "$API_CODE" = "200" ] && [ "$GRAFANA_CODE" = "200" ]; then + echo "API is reachable at $CHECK_URL_API (200)." + echo "Grafana is reachable at $CHECK_URL_GRAFANA (200)." + break + fi + sleep 2 + if [ "$attempt" -eq 30 ]; then + echo "timeout error (API=$API_CODE, Grafana=$GRAFANA_CODE)." >&2 + exit 1 + fi +done