pipekit/deploy.sh
Paul Trowbridge a3ff5337ee deploy.sh: chown only pipekit.db, not the whole repo
Avoids stripping write access from the developer. The service only needs
to own pipekit.db (runtime writes) and .venv (created as pipekit).
Source code stays owned by whoever ran deploy.sh.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-06-03 21:37:18 -04:00

209 lines
8.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# Pipekit deployment — idempotent. Re-run after any code update.
#
# What it does:
# 1. Creates the 'pipekit' system user and group (if absent)
# 2. Adds the invoking user to the 'pipekit' group
# 3. Sets ownership of /opt/pipekit to pipekit:pipekit (group-writable)
# 4. Creates Python venv (as pipekit) and installs requirements
# 5. Installs /usr/local/bin/pipekit launcher
# 6. Creates /etc/pipekit/secrets.env (mode 0640, group pipekit)
# 7. Runs 'pipekit init' to create/upgrade the SQLite schema
# 8. Registers JDBC driver rows for every jar shipped with jrunner
# 9. Installs and enables the systemd unit (does not start it)
#
# Usage:
# ./deploy.sh # re-execs itself with sudo if needed
#
# After deploy:
# sudo pipekit secrets set KEY VALUE # add connection passwords
# sudo systemctl start pipekit
set -euo pipefail
REPO_DIR="${PIPEKIT_REPO:-$(cd "$(dirname "$0")" && pwd)}"
VENV_DIR="$REPO_DIR/.venv"
LAUNCHER="/usr/local/bin/pipekit"
CONFIG_DIR="/etc/pipekit"
SECRETS_FILE="$CONFIG_DIR/secrets.env"
SERVICE_NAME="pipekit"
UNIT_SRC="$REPO_DIR/systemd/pipekit.service"
UNIT_DST="/etc/systemd/system/pipekit.service"
# Capture the invoking user before re-execing as root
INVOKING_USER="${SUDO_USER:-${USER:-}}"
# Re-exec as root if needed
if [ "$EUID" -ne 0 ]; then
exec sudo -E "$0" "$@"
fi
step() { echo ""; echo "── $* ──"; }
echo "== pipekit deploy =="
echo "repo: $REPO_DIR"
echo "venv: $VENV_DIR"
echo "secrets: $SECRETS_FILE"
echo "user: $SERVICE_NAME (invoking user: ${INVOKING_USER:-unknown})"
# ── Prerequisites ─────────────────────────────────────────────────────────────
step "Checking prerequisites"
command -v python3 >/dev/null || { echo "ERROR: python3 not on PATH"; exit 1; }
echo " python3: $(python3 --version)"
command -v jrunner >/dev/null || { echo "ERROR: jrunner not on PATH — install /opt/jrunner first"; exit 1; }
echo " jrunner: $(command -v jrunner)"
# ── 1. System user ────────────────────────────────────────────────────────────
step "System user"
if ! id -u "$SERVICE_NAME" >/dev/null 2>&1; then
echo " Creating system user: $SERVICE_NAME"
useradd --system --no-create-home --home-dir /nonexistent \
--shell /usr/sbin/nologin "$SERVICE_NAME"
echo " Done."
else
echo " User '$SERVICE_NAME' already exists (uid=$(id -u "$SERVICE_NAME"))."
fi
# ── 2. Add invoking user to pipekit group ─────────────────────────────────────
step "Group membership"
if [ -n "$INVOKING_USER" ] && [ "$INVOKING_USER" != "$SERVICE_NAME" ]; then
if id -nG "$INVOKING_USER" | grep -qw "$SERVICE_NAME"; then
echo " $INVOKING_USER is already in the '$SERVICE_NAME' group."
else
echo " Adding $INVOKING_USER to the '$SERVICE_NAME' group."
usermod -aG "$SERVICE_NAME" "$INVOKING_USER"
echo " Done. Log out and back in for this to take effect."
fi
else
echo " Skipping — could not determine invoking user."
fi
# ── 3. Ownership ──────────────────────────────────────────────────────────────
step "File ownership and permissions"
# Only chown what the service needs to write at runtime.
# Source code stays owned by the invoking user.
DB_FILE="$REPO_DIR/pipekit.db"
if [ -f "$DB_FILE" ]; then
echo " $DB_FILE$SERVICE_NAME"
chown "$SERVICE_NAME:$SERVICE_NAME" "$DB_FILE"
else
echo " $DB_FILE not yet created (pipekit init will create it as $SERVICE_NAME)"
fi
echo " $VENV_DIR$SERVICE_NAME (created/managed below)"
echo " Done."
# ── 4. Venv + deps ────────────────────────────────────────────────────────────
step "Python venv"
if [ -d "$VENV_DIR" ] && [ "$(stat -c '%U' "$VENV_DIR")" != "$SERVICE_NAME" ]; then
echo " Venv exists but is not owned by $SERVICE_NAME — recreating."
rm -rf "$VENV_DIR"
fi
if [ ! -d "$VENV_DIR" ]; then
echo " Creating venv at $VENV_DIR"
sudo -u "$SERVICE_NAME" HOME=/nonexistent python3 -m venv "$VENV_DIR"
else
echo " Venv already exists at $VENV_DIR."
fi
echo " Upgrading pip"
sudo -u "$SERVICE_NAME" HOME=/nonexistent \
"$VENV_DIR/bin/pip" install --quiet --no-cache-dir --upgrade pip
echo " Installing requirements"
sudo -u "$SERVICE_NAME" HOME=/nonexistent \
"$VENV_DIR/bin/pip" install --quiet --no-cache-dir -r "$REPO_DIR/requirements.txt"
echo " Done."
# ── 5. Launcher ───────────────────────────────────────────────────────────────
step "Launcher"
chmod +x "$REPO_DIR/bin/pipekit"
ln -sf "$REPO_DIR/bin/pipekit" "$LAUNCHER"
echo " $LAUNCHER -> $REPO_DIR/bin/pipekit"
# ── 6. Secrets file ───────────────────────────────────────────────────────────
step "Secrets file"
install -d -m 0755 "$CONFIG_DIR"
if [ ! -f "$SECRETS_FILE" ]; then
echo " Creating $SECRETS_FILE (mode 0640, group $SERVICE_NAME)"
install -m 0640 /dev/null "$SECRETS_FILE"
chown "root:$SERVICE_NAME" "$SECRETS_FILE"
cat > "$SECRETS_FILE" <<'EOF'
# pipekit secrets — loaded by the systemd unit as EnvironmentFile.
# Connection passwords are stored as $KEY references in the DB.
# Add entries with: sudo pipekit secrets set KEY VALUE
EOF
else
echo " $SECRETS_FILE already exists — keeping contents."
chown "root:$SERVICE_NAME" "$SECRETS_FILE"
chmod 0640 "$SECRETS_FILE"
echo " Permissions ensured: 0640 group $SERVICE_NAME."
fi
# ── 7. Schema init ────────────────────────────────────────────────────────────
step "Database schema"
DB_FILE="$REPO_DIR/pipekit.db"
if [ ! -f "$DB_FILE" ]; then
echo " Creating $DB_FILE owned by $SERVICE_NAME"
touch "$DB_FILE"
chown "$SERVICE_NAME:$SERVICE_NAME" "$DB_FILE"
fi
echo " Running pipekit init"
sudo -u "$SERVICE_NAME" HOME=/nonexistent "$LAUNCHER" init
echo " Done."
# ── 8. Driver registration ────────────────────────────────────────────────────
step "JDBC driver registration"
JR_LIB="$(dirname "$(readlink -f "$(command -v jrunner)")")/../lib"
echo " jrunner lib dir: $JR_LIB"
register_jar() {
local kind="$1" pattern="$2"
local jar
jar="$(find "$JR_LIB" -maxdepth 1 -name "$pattern" 2>/dev/null | head -1)"
if [ -n "$jar" ]; then
echo " Registering $kind: $(basename "$jar")"
sudo -u "$SERVICE_NAME" HOME=/nonexistent "$LAUNCHER" drivers register "$kind" --jar "$jar"
else
echo " No $pattern found in $JR_LIB — skipping $kind"
fi
}
register_jar db2 "jt400-*.jar"
register_jar pg "postgresql-*.jar"
register_jar mssql "mssql-jdbc-*.jar"
# ── 9. Systemd unit ───────────────────────────────────────────────────────────
step "Systemd unit"
if [ ! -f "$UNIT_SRC" ]; then
echo " WARNING: $UNIT_SRC not found — skipping"
else
echo " Installing $UNIT_DST"
cp "$UNIT_SRC" "$UNIT_DST"
echo " Running systemctl daemon-reload"
systemctl daemon-reload
echo " Enabling $SERVICE_NAME service"
systemctl enable "$SERVICE_NAME"
echo " Done."
fi
echo ""
echo "════════════════════════════════════"
echo " pipekit deployed successfully."
echo "════════════════════════════════════"
echo ""
echo "Next steps:"
echo " 1. Add connection passwords:"
echo " sudo pipekit secrets set DB2PW <value>"
echo " sudo pipekit secrets set PGPW <value>"
echo " 2. Start the service:"
echo " sudo systemctl start pipekit"
echo " 3. Check it:"
echo " sudo systemctl status pipekit"
echo " journalctl -u pipekit -f"
if [ -n "$INVOKING_USER" ]; then
if ! id -nG "$INVOKING_USER" 2>/dev/null | grep -qw "$SERVICE_NAME"; then
echo ""
echo " NOTE: $INVOKING_USER was added to the '$SERVICE_NAME' group."
echo " Log out and back in for write access to take effect."
fi
fi