netgescon-day0/scripts/install/netgescon-node-bootstrap.sh

467 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
APP_DIR="${APP_DIR:-/var/www/netgescon}"
GIT_SOURCE_DIR="${GIT_SOURCE_DIR:-/var/www/netgescon-git-source}"
SITE_KEY="${SITE_KEY:-}"
BRANCH="${BRANCH:-main}"
REPO_URL="${REPO_URL:-}"
APP_URL="${APP_URL:-http://127.0.0.1}"
SERVER_NAME="${SERVER_NAME:-_}"
DB_NAME="${DB_NAME:-netgescon}"
DB_USER="${DB_USER:-netgescon_user}"
DB_PASSWORD="${DB_PASSWORD:-}"
ADMIN_NAME="${ADMIN_NAME:-NetGescon Super Admin}"
ADMIN_EMAIL="${ADMIN_EMAIL:-admin@netgescon.local}"
ADMIN_PASSWORD="${ADMIN_PASSWORD:-}"
PHP_VERSION="${PHP_VERSION:-8.3}"
RUN_NPM_BUILD=1
RUN_MIGRATIONS=1
ALLOW_EXISTING_APP_DIR=0
ALLOW_EXISTING_DATABASE=0
REPLACE_NGINX_SITE=0
usage() {
cat <<'EOF'
Usage: netgescon-node-bootstrap.sh [options]
Required options:
--repo-url <url> Git repository URL to clone as node source
--db-password <password> MySQL password for the application user
--admin-password <pwd> Password for the initial super-admin user
Optional options:
--site-key <slug> Unique site key used for nginx/cron/supervisor names
--branch <name> Git branch to checkout (default: main)
--app-dir <path> Laravel application directory (default: /var/www/netgescon)
--git-source-dir <path> Local Git source directory for browser sync (default: /var/www/netgescon-git-source)
--app-url <url> Public APP_URL value (default: http://127.0.0.1)
--server-name <name> Nginx server_name value (default: _)
--db-name <name> MySQL database name (default: netgescon)
--db-user <user> MySQL app user (default: netgescon_user)
--admin-email <email> Initial super-admin email (default: admin@netgescon.local)
--admin-name <name> Initial super-admin display name
--php-version <version> PHP FPM version for nginx upstream (default: 8.3)
--skip-build Skip npm install/build
--skip-migrations Skip artisan migrate and admin bootstrap
--allow-existing-app-dir Reuse an existing git worktree in the target app dir
--allow-existing-database Reuse an existing database/user instead of failing
--replace-nginx-site Replace an existing nginx site file for this site key
--help Show this message
Example:
sudo bash scripts/install/netgescon-node-bootstrap.sh \
--repo-url git@your-gitea:studio/netgescon.git \
--app-url https://netgescon.example.it \
--server-name netgescon.example.it \
--db-password 'StrongDbPass123!' \
--admin-password 'StrongAdminPass123!'
EOF
}
log() {
local level="$1"
shift
printf '[%s] %s\n' "$level" "$*"
}
info() { log INFO "$@"; }
warn() { log WARN "$@"; }
fail() { log ERROR "$@"; exit 1; }
slugify() {
local value="$1"
value=$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]')
value=$(printf '%s' "$value" | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')
if [[ -z "$value" ]]; then
value='netgescon'
fi
printf '%s' "$value"
}
run_root() {
if [[ ${EUID} -eq 0 ]]; then
"$@"
else
sudo "$@"
fi
}
sql_escape() {
printf '%s' "$1" | sed "s/'/''/g"
}
php_escape() {
printf '%s' "$1" | sed "s/'/\\'/g"
}
mysql_database_exists() {
run_root mysql -Nse "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$1'" | grep -qx "$1"
}
mysql_user_exists() {
run_root mysql -Nse "SELECT User FROM mysql.user WHERE User = '$1' AND Host = 'localhost'" | grep -qx "$1"
}
set_env_value() {
local key="$1"
local value="$2"
local file="$3"
local escaped
escaped=$(printf '%s' "$value" | sed -e 's/[\/&]/\\&/g')
if grep -q "^${key}=" "$file"; then
sed -i "s/^${key}=.*/${key}=${escaped}/" "$file"
else
printf '%s=%s\n' "$key" "$value" >> "$file"
fi
}
while [[ $# -gt 0 ]]; do
case "$1" in
--site-key)
SITE_KEY="$2"
shift 2
;;
--repo-url)
REPO_URL="$2"
shift 2
;;
--branch)
BRANCH="$2"
shift 2
;;
--app-dir)
APP_DIR="$2"
shift 2
;;
--git-source-dir)
GIT_SOURCE_DIR="$2"
shift 2
;;
--app-url)
APP_URL="$2"
shift 2
;;
--server-name)
SERVER_NAME="$2"
shift 2
;;
--db-name)
DB_NAME="$2"
shift 2
;;
--db-user)
DB_USER="$2"
shift 2
;;
--db-password)
DB_PASSWORD="$2"
shift 2
;;
--admin-email)
ADMIN_EMAIL="$2"
shift 2
;;
--admin-name)
ADMIN_NAME="$2"
shift 2
;;
--admin-password)
ADMIN_PASSWORD="$2"
shift 2
;;
--php-version)
PHP_VERSION="$2"
shift 2
;;
--skip-build)
RUN_NPM_BUILD=0
shift
;;
--skip-migrations)
RUN_MIGRATIONS=0
shift
;;
--allow-existing-app-dir)
ALLOW_EXISTING_APP_DIR=1
shift
;;
--allow-existing-database)
ALLOW_EXISTING_DATABASE=1
shift
;;
--replace-nginx-site)
REPLACE_NGINX_SITE=1
shift
;;
--help|-h)
usage
exit 0
;;
*)
fail "Unknown option: $1"
;;
esac
done
if [[ -z "$REPO_URL" ]]; then
fail "--repo-url is required"
fi
if [[ -z "$DB_PASSWORD" ]]; then
fail "--db-password is required"
fi
if [[ -z "$ADMIN_PASSWORD" && $RUN_MIGRATIONS -eq 1 ]]; then
fail "--admin-password is required unless --skip-migrations is used"
fi
for command in git php composer mysql nginx; do
if ! command -v "$command" >/dev/null 2>&1; then
fail "Missing required command: $command"
fi
done
if [[ -z "$SITE_KEY" ]]; then
SITE_KEY="$(slugify "$(basename "$APP_DIR")")"
else
SITE_KEY="$(slugify "$SITE_KEY")"
fi
APP_PARENT_DIR="$(dirname "$APP_DIR")"
NGINX_SITE_FILE="/etc/nginx/sites-available/${SITE_KEY}.conf"
NGINX_SITE_ENABLED="/etc/nginx/sites-enabled/${SITE_KEY}.conf"
SUPERVISOR_FILE="/etc/supervisor/conf.d/${SITE_KEY}-worker.conf"
CRON_FILE="/etc/cron.d/${SITE_KEY}-scheduler"
TEMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TEMP_DIR"' EXIT
if [[ -d "$APP_DIR" ]] && [[ -n "$(find "$APP_DIR" -mindepth 1 -maxdepth 1 2>/dev/null | head -n 1)" ]]; then
if [[ $ALLOW_EXISTING_APP_DIR -ne 1 ]]; then
fail "App directory already exists and is not empty: $APP_DIR. Re-run with --allow-existing-app-dir only if you really want to reuse it."
fi
if [[ ! -d "$APP_DIR/.git" || ! -f "$APP_DIR/artisan" ]]; then
fail "Existing app directory must already be a Laravel git worktree: $APP_DIR"
fi
fi
if [[ -d "$GIT_SOURCE_DIR" ]] && [[ ! -d "$GIT_SOURCE_DIR/.git" ]] && [[ -n "$(find "$GIT_SOURCE_DIR" -mindepth 1 -maxdepth 1 2>/dev/null | head -n 1)" ]]; then
fail "Git source directory exists and is not a git repository: $GIT_SOURCE_DIR"
fi
if [[ -e "$NGINX_SITE_FILE" || -L "$NGINX_SITE_ENABLED" ]]; then
if [[ $REPLACE_NGINX_SITE -ne 1 ]]; then
fail "Nginx site already exists for ${SITE_KEY}. Re-run with --replace-nginx-site only if replacement is intentional."
fi
fi
if mysql_database_exists "$DB_NAME"; then
if [[ $ALLOW_EXISTING_DATABASE -ne 1 ]]; then
fail "Database already exists: $DB_NAME. Re-run with --allow-existing-database only if reuse is intentional."
fi
fi
if mysql_user_exists "$DB_USER"; then
if [[ $ALLOW_EXISTING_DATABASE -ne 1 ]]; then
fail "Database user already exists: $DB_USER. Re-run with --allow-existing-database only if reuse is intentional."
fi
fi
info "Preparing directories"
run_root install -d -o "${SUDO_USER:-$USER}" -g www-data -m 2775 "$APP_PARENT_DIR"
run_root install -d -o "${SUDO_USER:-$USER}" -g www-data -m 2775 "$(dirname "$GIT_SOURCE_DIR")"
if [[ -d "$GIT_SOURCE_DIR/.git" ]]; then
info "Refreshing local source repository"
git -C "$GIT_SOURCE_DIR" remote set-url origin "$REPO_URL"
git -C "$GIT_SOURCE_DIR" fetch --all --prune
git -C "$GIT_SOURCE_DIR" checkout "$BRANCH"
git -C "$GIT_SOURCE_DIR" reset --hard "origin/$BRANCH"
else
info "Cloning local source repository"
git clone --branch "$BRANCH" "$REPO_URL" "$GIT_SOURCE_DIR"
fi
if [[ -d "$APP_DIR/.git" ]]; then
info "Refreshing application worktree"
git -C "$APP_DIR" remote set-url origin "$REPO_URL"
git -C "$APP_DIR" fetch --all --prune
git -C "$APP_DIR" checkout "$BRANCH"
git -C "$APP_DIR" reset --hard "origin/$BRANCH"
else
info "Cloning application worktree"
git clone --branch "$BRANCH" "$GIT_SOURCE_DIR" "$APP_DIR"
git -C "$APP_DIR" remote set-url origin "$REPO_URL"
fi
run_root git config --global --add safe.directory "$APP_DIR"
run_root git config --global --add safe.directory "$GIT_SOURCE_DIR"
if [[ ! -f "$APP_DIR/artisan" ]]; then
fail "Laravel artisan file not found in $APP_DIR"
fi
info "Configuring MySQL database and user"
DB_PASSWORD_ESCAPED="$(sql_escape "$DB_PASSWORD")"
run_root mysql <<SQL
CREATE DATABASE IF NOT EXISTS \
\\`$DB_NAME\\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASSWORD_ESCAPED';
ALTER USER '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASSWORD_ESCAPED';
GRANT ALL PRIVILEGES ON \\`$DB_NAME\\`.* TO '$DB_USER'@'localhost';
FLUSH PRIVILEGES;
SQL
info "Installing PHP dependencies"
cd "$APP_DIR"
composer install --no-dev --optimize-autoloader
if [[ $RUN_NPM_BUILD -eq 1 && -f package.json ]]; then
info "Installing frontend dependencies"
if [[ -f package-lock.json ]]; then
npm ci
else
npm install
fi
npm run build
fi
if [[ ! -f .env ]]; then
if [[ -f .env.example ]]; then
cp .env.example .env
else
fail ".env and .env.example are both missing"
fi
fi
info "Configuring Laravel environment"
set_env_value APP_NAME NetGescon .env
set_env_value APP_ENV production .env
set_env_value APP_DEBUG false .env
set_env_value APP_URL "$APP_URL" .env
set_env_value DB_CONNECTION mysql .env
set_env_value DB_HOST 127.0.0.1 .env
set_env_value DB_PORT 3306 .env
set_env_value DB_DATABASE "$DB_NAME" .env
set_env_value DB_USERNAME "$DB_USER" .env
set_env_value DB_PASSWORD "$DB_PASSWORD" .env
set_env_value CACHE_STORE file .env
set_env_value SESSION_DRIVER file .env
set_env_value QUEUE_CONNECTION database .env
set_env_value FILESYSTEM_DISK public .env
set_env_value NETGESCON_GIT_SYNC_SOURCE_REPO "$GIT_SOURCE_DIR" .env
mkdir -p storage/app/support
ln -sfn "$GIT_SOURCE_DIR" storage/app/support/git-source
php artisan key:generate --force
php artisan storage:link || true
php artisan optimize:clear
if [[ $RUN_MIGRATIONS -eq 1 ]]; then
info "Running migrations"
php artisan migrate --force
info "Creating initial super-admin user"
ADMIN_NAME_ESCAPED="$(php_escape "$ADMIN_NAME")"
ADMIN_EMAIL_ESCAPED="$(php_escape "$ADMIN_EMAIL")"
ADMIN_PASSWORD_ESCAPED="$(php_escape "$ADMIN_PASSWORD")"
php artisan tinker --execute="use App\\Models\\User; use Illuminate\\Support\\Facades\\Hash; use Spatie\\Permission\\Models\\Role; \\$role = Role::firstOrCreate(['name' => 'super-admin', 'guard_name' => 'web']); \\$adminRole = Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']); \\$user = User::updateOrCreate(['email' => '$ADMIN_EMAIL_ESCAPED'], ['name' => '$ADMIN_NAME_ESCAPED', 'email_verified_at' => now(), 'password' => Hash::make('$ADMIN_PASSWORD_ESCAPED'), 'is_active' => true, 'registration_status' => 'approved']); \\$user->syncRoles(['super-admin', 'admin']);"
fi
info "Caching Laravel configuration"
php artisan config:cache
php artisan route:cache
php artisan view:cache
info "Setting permissions"
run_root chown -R www-data:www-data "$APP_DIR"
run_root chmod -R 775 "$APP_DIR/storage" "$APP_DIR/bootstrap/cache"
info "Writing nginx virtual host"
cat > "$TEMP_DIR/${SITE_KEY}.conf" <<EOF
server {
listen 80;
listen [::]:80;
server_name $SERVER_NAME;
root $APP_DIR/public;
index index.php index.html;
client_max_body_size 64m;
access_log /var/log/nginx/${SITE_KEY}-access.log;
error_log /var/log/nginx/${SITE_KEY}-error.log;
location / {
try_files \\$uri \\$uri/ /index.php?\$query_string;
}
location ~ \\.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php${PHP_VERSION}-fpm.sock;
fastcgi_param SCRIPT_FILENAME \\$realpath_root\$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\\.(?!well-known).* {
deny all;
}
}
EOF
run_root install -m 0644 "$TEMP_DIR/${SITE_KEY}.conf" "$NGINX_SITE_FILE"
run_root ln -sfn "$NGINX_SITE_FILE" "$NGINX_SITE_ENABLED"
run_root nginx -t
run_root systemctl reload nginx
info "Configuring scheduler cron"
cat > "$TEMP_DIR/${SITE_KEY}-scheduler" <<EOF
* * * * * www-data cd $APP_DIR && php artisan schedule:run >> /var/log/${SITE_KEY}-scheduler.log 2>&1
EOF
run_root install -m 0644 "$TEMP_DIR/${SITE_KEY}-scheduler" "$CRON_FILE"
info "Configuring supervisor worker"
cat > "$TEMP_DIR/${SITE_KEY}-worker.conf" <<EOF
[program:${SITE_KEY}-worker]
command=php $APP_DIR/artisan queue:work --sleep=3 --tries=3 --max-time=3600
directory=$APP_DIR
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/${SITE_KEY}-worker.log
stopwaitsecs=3600
EOF
run_root install -m 0644 "$TEMP_DIR/${SITE_KEY}-worker.conf" "$SUPERVISOR_FILE"
run_root supervisorctl reread
run_root supervisorctl update
run_root supervisorctl restart "${SITE_KEY}-worker" || true
SUMMARY_FILE="$APP_DIR/install-node-summary.txt"
cat > "$TEMP_DIR/install-node-summary.txt" <<EOF
NetGescon node bootstrap completed
Date: $(date -Is)
Site key: $SITE_KEY
Application dir: $APP_DIR
Git source dir: $GIT_SOURCE_DIR
Branch: $BRANCH
APP_URL: $APP_URL
Server name: $SERVER_NAME
Database: $DB_NAME
Database user: $DB_USER
Admin email: $ADMIN_EMAIL
Update launcher: $APP_URL/admin-filament/supporto/aggiornamento-nodo
EOF
run_root install -o www-data -g www-data -m 0644 "$TEMP_DIR/install-node-summary.txt" "$SUMMARY_FILE"
info "NetGescon node bootstrap completed"
printf '\nOpen after first login:\n'
printf ' %s/admin-filament/supporto/aggiornamento-nodo\n' "$APP_URL"
printf '\nSummary file:\n'
printf ' %s\n' "$SUMMARY_FILE"