feat: Pod-Skript finance_pod mit systemd-Service und Secrets-Bootstrap
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
202
create_pod_finance.sh
Executable file
202
create_pod_finance.sh
Executable file
@@ -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" <<EOF
|
||||
POSTGRES_PASSWORD=$(openssl rand -hex 16)
|
||||
FINANCE_READ_PASSWORD=$(openssl rand -hex 16)
|
||||
FB_API_KEY=$(openssl rand -hex 32)
|
||||
FB_SESSION_SECRET=$(openssl rand -hex 32)
|
||||
FB_GUI_USER=admin
|
||||
FB_GUI_PASSWORD_HASH=$(python3 -c "import hashlib,secrets;s=secrets.token_hex(16);print(s+'\$'+hashlib.pbkdf2_hmac('sha256',b'$GUI_PASSWORD',s.encode(),200000).hex())")
|
||||
GRAFANA_ADMIN_PASSWORD=$(openssl rand -base64 12)
|
||||
EOF
|
||||
chmod 600 "$ENV_FILE"
|
||||
echo "NEU ERZEUGT: GUI-Login admin / $GUI_PASSWORD (jetzt notieren!)"
|
||||
echo "Grafana-Login admin / siehe GRAFANA_ADMIN_PASSWORD in $ENV_FILE"
|
||||
fi
|
||||
set -a; . "$ENV_FILE"; set +a
|
||||
|
||||
# Stop existing systemd-managed pod if present, to avoid conflicts on rerun
|
||||
echo "Stopping systemd-managed pod 'pod-$POD_NAME.service' if it exists..."
|
||||
if systemctl --user list-units --type=service --all 2>/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
|
||||
Reference in New Issue
Block a user