Private
Public Access
1
0
Files
linux_patch_manager/docker/entrypoint.sh
Draco-Lunaris-Echo 9f74d2ccf2 feat: add automated install, Docker deployment, and CI Docker job
- debian/control: add Pre-Depends and Depends on postgresql-16, argon2
- debian/postinst: idempotent automation for PostgreSQL setup,
  DB/user creation, migration tracking, admin password generation,
  config write, and service enable/start
- Dockerfile: multi-stage build (Rust + frontend + slim runtime)
- docker/entrypoint.sh: first-run DB wait, migrations, admin password
- docker-compose.yml: split db/app architecture with healthcheck
- .env.example: template for DB_PASSWORD and TAG
- .dockerignore: exclude build artifacts from Docker context
- .github/workflows/ci.yml: add Docker job for multi-arch
  (amd64/arm64) GHCR push on tag releases with layer caching
- .gitignore: add .env entry
2026-06-07 15:27:07 -05:00

233 lines
9.0 KiB
Bash
Executable File

#!/bin/bash
set -e
# =============================================================================
# Linux Patch Manager — Docker Entrypoint
# =============================================================================
# Handles first-run: wait for DB, run migrations, generate admin password,
# start pm-web and pm-worker services.
# =============================================================================
MIGRATION_DIR="/usr/share/patch-manager/migrations"
CONFIG_DIR="/etc/patch-manager"
ADMIN_PASSWORD_FILE="/etc/patch-manager/admin-password.txt"
# ---------------------------------------------------------------------------
# Parse DATABASE_URL into PG* env vars for psql compatibility
# ---------------------------------------------------------------------------
parse_database_url() {
# DATABASE_URL format: postgres://user:password@host:port/dbname
local url="${DATABASE_URL}"
# Extract components
DB_PASS=$(echo "$url" | sed -n 's|postgres://[^:]*:\([^@]*\)@.*|\1|p')
DB_HOST=$(echo "$url" | sed -n 's|.*@\([^:/]*\).*|\1|p')
DB_PORT=$(echo "$url" | sed -n 's|.*:\([0-9]*\)/.*|\1|p')
DB_USER=$(echo "$url" | sed -n 's|postgres://\([^:]*\):.*|\1|p')
DB_NAME=$(echo "$url" | sed -n 's|.*/\([^?]*\).*|\1|p')
# Default port
DB_PORT="${DB_PORT:-5432}"
export PGHOST="${DB_HOST}"
export PGPORT="${DB_PORT}"
export PGUSER="${DB_USER}"
export PGPASSWORD="${DB_PASS}"
export PGDATABASE="${DB_NAME}"
}
# ---------------------------------------------------------------------------
# Wait for PostgreSQL to be ready
# ---------------------------------------------------------------------------
wait_for_db() {
echo "[entrypoint] Waiting for PostgreSQL at ${PGHOST}:${DB_PORT}..."
local retries=60
local delay=2
local i
for ((i = 1; i <= retries; i++)); do
if pg_isready -q -h "${PGHOST}" -p "${DB_PORT}" -U "${DB_USER}" 2>/dev/null; then
echo "[entrypoint] PostgreSQL is ready."
return 0
fi
echo "[entrypoint] PostgreSQL not ready (attempt ${i}/${retries}), waiting ${delay}s..."
sleep "${delay}"
done
echo "[entrypoint] ERROR: PostgreSQL did not become ready after $((retries * delay)) seconds." >&2
return 1
}
# ---------------------------------------------------------------------------
# Run database migrations (idempotent)
# ---------------------------------------------------------------------------
run_migrations() {
echo "[entrypoint] Applying database migrations..."
# Ensure pgcrypto extension
psql -v ON_ERROR_STOP=1 -c "CREATE EXTENSION IF NOT EXISTS pgcrypto;" 2>/dev/null || true
# Create migration tracking table
psql -v ON_ERROR_STOP=1 <<'EOSQL'
CREATE TABLE IF NOT EXISTS _migrations (
id SERIAL PRIMARY KEY,
filename TEXT NOT NULL UNIQUE,
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
EOSQL
# Handle upgrade from pre-migration-tracking versions
local migration_count
migration_count=$(psql -t -A -c "SELECT COUNT(*) FROM _migrations;" 2>/dev/null || echo "0")
migration_count="${migration_count// /}"
local tables_exist
tables_exist=$(psql -t -A -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public' AND table_name='users';" 2>/dev/null || echo "0")
tables_exist="${tables_exist// /}"
if [[ "${migration_count}" == "0" && "${tables_exist}" -gt 0 ]]; then
echo "[entrypoint] Existing database detected — marking all shipped migrations as already applied."
for sql_file in $(ls "${MIGRATION_DIR}"/*.sql 2>/dev/null | sort); do
local fname
fname=$(basename "${sql_file}")
psql -v ON_ERROR_STOP=1 -c "INSERT INTO _migrations (filename) VALUES ('${fname}') ON CONFLICT (filename) DO NOTHING;" 2>/dev/null || true
done
fi
# Apply each migration in sorted order
local applied=0
local skipped=0
for sql_file in $(ls "${MIGRATION_DIR}"/*.sql 2>/dev/null | sort); do
local fname
fname=$(basename "${sql_file}")
local already_applied
already_applied=$(psql -t -A -c "SELECT COUNT(*) FROM _migrations WHERE filename='${fname}';" 2>/dev/null || echo "0")
already_applied="${already_applied// /}"
if [[ "${already_applied}" -gt 0 ]]; then
skipped=$((skipped + 1))
continue
fi
echo "[entrypoint] Applying migration: ${fname}"
if psql -v ON_ERROR_STOP=1 -f "${sql_file}"; then
psql -v ON_ERROR_STOP=1 -c "INSERT INTO _migrations (filename) VALUES ('${fname}');" 2>/dev/null || true
applied=$((applied + 1))
else
echo "[entrypoint] ERROR: Failed to apply migration: ${fname}" >&2
return 1
fi
done
if [[ "${applied}" -gt 0 ]]; then
echo "[entrypoint] Applied ${applied} new migration(s), skipped ${skipped}."
else
echo "[entrypoint] All migrations up to date (${skipped} already applied)."
fi
}
# ---------------------------------------------------------------------------
# Generate admin password on first run
# ---------------------------------------------------------------------------
generate_admin_password() {
echo "[entrypoint] Checking admin password status..."
# Generate a random 24-character password
local admin_password
admin_password=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9!@#%^&*' | head -c 24)
# Hash with argon2 (PHC format)
local password_hash
password_hash=$(echo -n "${admin_password}" | argon2 salt -id -t 3 -m 65536 -p 1 -l 32 -e)
# Update admin user — only if placeholder hash is still present
# The placeholder starts with: $argon2id$v=19$m=65536,t=3,p=1$AAAAAAAAAAAAAAAA
# Using single-quoted variable to preserve $ signs in the SQL LIKE pattern
local placeholder_pattern
placeholder_pattern='$argon2id$v=19$m=65536,t=3,p=1$AAAAAAAAAAAAAAAA%'
local updated
updated=$(psql -t -A -c \
"UPDATE users SET password_hash = '${password_hash}', force_password_reset = TRUE \
WHERE username = 'admin' AND password_hash LIKE '${placeholder_pattern}' \
RETURNING id;" 2>/dev/null || echo "")
if [[ -n "${updated}" ]]; then
echo "${admin_password}" > "${ADMIN_PASSWORD_FILE}"
chmod 600 "${ADMIN_PASSWORD_FILE}"
chown root:root "${ADMIN_PASSWORD_FILE}"
echo ""
echo "============================================="
echo " Linux Patch Manager — Admin Credentials"
echo "============================================="
echo " Username: admin"
echo " Password: ${admin_password}"
echo ""
echo " IMPORTANT: Save this password! It will not be shown again."
echo " Password also saved to: ${ADMIN_PASSWORD_FILE}"
echo "============================================="
echo ""
else
echo "[entrypoint] Admin password already set (not a fresh install)."
fi
}
# ---------------------------------------------------------------------------
# Generate JWT keys if not present
# ---------------------------------------------------------------------------
generate_jwt_keys() {
if [[ ! -f "${CONFIG_DIR}/jwt/signing.pem" ]]; then
echo "[entrypoint] Generating Ed25519 JWT signing key..."
openssl genpkey -algorithm ed25519 -out "${CONFIG_DIR}/jwt/signing.pem" 2>/dev/null
openssl pkey -in "${CONFIG_DIR}/jwt/signing.pem" -pubout -out "${CONFIG_DIR}/jwt/verify.pem" 2>/dev/null
chown patch-manager:patch-manager "${CONFIG_DIR}/jwt/signing.pem" "${CONFIG_DIR}/jwt/verify.pem"
chmod 600 "${CONFIG_DIR}/jwt/signing.pem"
chmod 644 "${CONFIG_DIR}/jwt/verify.pem"
echo "[entrypoint] JWT keys generated."
else
echo "[entrypoint] JWT signing key already exists."
fi
}
# ---------------------------------------------------------------------------
# Write config.toml if not present
# ---------------------------------------------------------------------------
write_config() {
local config_file="${CONFIG_DIR}/config.toml"
if [[ -f "${config_file}" ]]; then
echo "[entrypoint] Config file already exists, not overwriting."
return 0
fi
echo "[entrypoint] Writing configuration file..."
cp /usr/share/patch-manager/config.example.toml "${config_file}"
sed -i "s|postgres://patch_manager:CHANGEME@localhost/patch_manager|${DATABASE_URL}|" "${config_file}"
chown patch-manager:patch-manager "${config_file}"
chmod 640 "${config_file}"
echo "[entrypoint] Configuration written."
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
echo "[entrypoint] Linux Patch Manager Docker entrypoint starting..."
parse_database_url
wait_for_db
run_migrations
generate_admin_password
generate_jwt_keys
write_config
echo "[entrypoint] Starting pm-web and pm-worker..."
# Start pm-worker in background
pm-worker &
WORKER_PID=$!
# Start pm-web in foreground (main process)
export PATCH_MANAGER_CONFIG="${CONFIG_DIR}/config.toml"
exec pm-web