- deploy.sh: set /etc/pipekit to root:pipekit 0775 and secrets.env to pipekit:pipekit 0640 so group members can run 'pipekit secrets set' without sudo - cli.py secrets set: drop os.chown() on temp file — non-root users can't chown to the pipekit service user, and os.replace() preserves the target file's ownership anyway Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
239 lines
10 KiB
Bash
Executable File
239 lines
10 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"
|
|
# Source code stays owned by the invoking user.
|
|
# pipekit needs to own pipekit.db and write to the repo directory
|
|
# (SQLite creates journal files alongside the db file).
|
|
DB_FILE="$REPO_DIR/pipekit.db"
|
|
if [ -f "$DB_FILE" ]; then
|
|
echo " $DB_FILE → $SERVICE_NAME:$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 " $REPO_DIR (directory only) → group $SERVICE_NAME, group-writable"
|
|
chgrp "$SERVICE_NAME" "$REPO_DIR"
|
|
chmod g+w "$REPO_DIR"
|
|
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"
|
|
mkdir -p "$VENV_DIR"
|
|
chown "$SERVICE_NAME:$SERVICE_NAME" "$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 0775 "$CONFIG_DIR"
|
|
chown "root:$SERVICE_NAME" "$CONFIG_DIR"
|
|
if [ ! -f "$SECRETS_FILE" ]; then
|
|
echo " Creating $SECRETS_FILE (mode 0640, group $SERVICE_NAME)"
|
|
install -m 0640 /dev/null "$SECRETS_FILE"
|
|
chown "$SERVICE_NAME:$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 "$SERVICE_NAME:$SERVICE_NAME" "$SECRETS_FILE"
|
|
chmod 0640 "$SECRETS_FILE"
|
|
echo " Permissions ensured: 0640 owner $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
|
|
# Detect JAVA_HOME — check PATH first, then search each location individually
|
|
JAVA_BIN="$(command -v java 2>/dev/null || true)"
|
|
if [ -z "$JAVA_BIN" ]; then
|
|
for _dir in /opt /usr/lib/jvm /usr/local/lib/jvm; do
|
|
[ -d "$_dir" ] || continue
|
|
JAVA_BIN="$(find "$_dir" -maxdepth 3 -name java -type f 2>/dev/null | head -1 || true)"
|
|
[ -n "$JAVA_BIN" ] && break
|
|
done
|
|
fi
|
|
if [ -z "$JAVA_BIN" ]; then
|
|
echo " WARNING: java not found — JAVA_HOME will not be set in unit"
|
|
JAVA_HOME_DETECTED=""
|
|
else
|
|
JAVA_HOME_DETECTED="$(dirname "$(dirname "$(readlink -f "$JAVA_BIN")")")"
|
|
echo " Detected JAVA_HOME: $JAVA_HOME_DETECTED"
|
|
fi
|
|
|
|
echo " Installing $UNIT_DST"
|
|
if [ -n "$JAVA_HOME_DETECTED" ]; then
|
|
# Inject Environment lines after WorkingDirectory= using printf to handle newlines portably
|
|
ENV_BLOCK="$(printf 'Environment=JAVA_HOME=%s\nEnvironment=PATH=%s/bin:/usr/local/bin:/usr/bin:/bin' "$JAVA_HOME_DETECTED" "$JAVA_HOME_DETECTED")"
|
|
awk -v env="$ENV_BLOCK" '/^WorkingDirectory=/{print; print env; next}1' "$UNIT_SRC" > "$UNIT_DST"
|
|
else
|
|
cp "$UNIT_SRC" "$UNIT_DST"
|
|
fi
|
|
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
|