feat: feature registry, auto-deploy, IP management, Docker support

Feature Manager (70+ features across 20 categories):
- Web servers: Apache2, nginx, OpenLiteSpeed, Varnish
- PHP: 7.4/8.1/8.2/8.3 multi-version, Composer
- Databases: MySQL 8, MariaDB, PostgreSQL, Redis, Memcached, phpMyAdmin, phpPgAdmin
- Email: Postfix, Dovecot, Roundcube, RainLoop, SpamAssassin, Rspamd, DKIM
- DNS: BIND9, PowerDNS
- FTP: ProFTPD, vsftpd, Pure-FTPd
- SSL: Certbot/Let's Encrypt, acme.sh
- Security: Fail2Ban, ModSecurity WAF, ImunifyAV, ClamAV, UFW, CrowdSec
- Containers: Docker Engine, Docker Compose, Portainer CE, per-account Docker hosting
- IP Management: Shared IPs (SNI), Dedicated IPs, IPv6
- Monitoring: Netdata, AWStats, GoAccess, Grafana+Prometheus
- Backup: BorgBackup, rclone (S3/B2/GCS), Duplicati
- CDN: Cloudflare API, PageSpeed Module
- Dev: Gitea, Phusion Passenger, JupyterHub
- One-click apps: WordPress+WP-CLI, auto-installer (50+ apps)
- Billing: WHMCS bridge, BoxBilling
- Reseller: White label, custom nameservers
- Notifications: Email, Slack, Telegram
- Compliance: Auditd, OSSEC HIDS

Auto-deploy pipeline (deploy/):
- webhook.php: HMAC-verified GitHub push webhook
- deploy-runner.sh: PHP syntax validation → git pull → rsync → DB migrations → PHP-FPM reload
- setup-deploy.sh: one-shot setup script, outputs GitHub webhook config
- Runs every minute via cron; locked to prevent concurrent deploys

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 05:11:36 +00:00
parent e802443d4a
commit e94dc719c8
6 changed files with 668 additions and 0 deletions
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# NovaCPX Deploy Runner — runs every minute via cron
# Processes /tmp/novacpx-deploy-queue.txt
# Each line: repo_path|web_root|commit
QUEUE="/tmp/novacpx-deploy-queue.txt"
LOG="/var/log/novacpx/deploy.log"
LOCK="/tmp/novacpx-deploy.lock"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG"; }
[[ ! -s "$QUEUE" ]] && exit 0
# Prevent concurrent runs
exec 9>"$LOCK"
flock -n 9 || { log "Deploy already running, skipping"; exit 0; }
while IFS='|' read -r REPO_PATH WEB_ROOT COMMIT; do
[[ -z "$REPO_PATH" ]] && continue
log "--- Deploying commit $COMMIT ---"
# Validate PHP syntax before applying
cd "$REPO_PATH" || continue
git fetch origin >> "$LOG" 2>&1
# Check PHP syntax on changed .php files
CHANGED_PHP=$(git diff HEAD..origin/main --name-only 2>/dev/null | grep '\.php$' || true)
SYNTAX_OK=true
for f in $CHANGED_PHP; do
[[ -f "$REPO_PATH/$f" ]] || continue
if ! php8.3 -l "$REPO_PATH/$f" >> "$LOG" 2>&1; then
log "SYNTAX ERROR in $f — aborting deploy"
SYNTAX_OK=false
break
fi
done
if ! $SYNTAX_OK; then
log "Deploy aborted due to PHP syntax errors"
continue
fi
# Pull
BEFORE=$(git rev-parse HEAD)
git pull origin main >> "$LOG" 2>&1
AFTER=$(git rev-parse HEAD)
if [[ "$BEFORE" == "$AFTER" ]]; then
log "Nothing new to deploy (already at $AFTER)"
continue
fi
log "Updated: $BEFORE$AFTER"
# Sync panel files to web root
rsync -av --delete \
--exclude='.git' \
--exclude='api/config.php' \
--exclude='*.log' \
"$REPO_PATH/panel/public/" "$WEB_ROOT/" >> "$LOG" 2>&1
# Run pending DB migrations
MIGR_DIR="$REPO_PATH/db/migrations"
if [[ -d "$MIGR_DIR" ]]; then
DB_NAME=$(python3 -c "import configparser; c=configparser.ConfigParser(); c.read('/etc/novacpx/config.ini'); print(c['database']['name'])" 2>/dev/null)
DB_USER=$(python3 -c "import configparser; c=configparser.ConfigParser(); c.read('/etc/novacpx/config.ini'); print(c['database']['user'])" 2>/dev/null)
DB_PASS=$(python3 -c "import configparser; c=configparser.ConfigParser(); c.read('/etc/novacpx/config.ini'); print(c['database']['pass'])" 2>/dev/null)
for SQL in "$MIGR_DIR"/*.sql; do
[[ -f "$SQL" ]] || continue
MIGR_NAME=$(basename "$SQL" .sql)
ALREADY=$(mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -se "SELECT value FROM settings WHERE \`key\`='migration_$MIGR_NAME'" 2>/dev/null)
if [[ -z "$ALREADY" ]]; then
log "Running migration: $MIGR_NAME"
mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" < "$SQL" >> "$LOG" 2>&1
mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "INSERT INTO settings (\`key\`,\`value\`) VALUES ('migration_$MIGR_NAME','$(date)') ON DUPLICATE KEY UPDATE \`value\`='$(date)'" 2>/dev/null
fi
done
fi
# Update VERSION
git describe --tags --abbrev=0 2>/dev/null > "$REPO_PATH/VERSION" || git rev-parse --short HEAD > "$REPO_PATH/VERSION"
# Restart PHP-FPM to pick up code changes
systemctl reload php8.3-fpm 2>/dev/null || true
systemctl reload php8.2-fpm 2>/dev/null || true
log "Deploy complete: $AFTER"
done < "$QUEUE"
# Clear queue
> "$QUEUE"
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Run once after install to configure the auto-deploy system
# Usage: bash setup-deploy.sh <github_webhook_secret>
set -euo pipefail
SECRET="${1:-$(openssl rand -hex 16)}"
REPO_PATH="/opt/novacpx-src"
WEB_ROOT="/srv/novacpx/public"
# Add deploy config to /etc/novacpx/config.ini
python3 - <<PYEOF
import configparser, os
cfg = configparser.ConfigParser()
cfg.read('/etc/novacpx/config.ini')
if 'deploy' not in cfg: cfg['deploy'] = {}
cfg['deploy']['webhook_secret'] = '$SECRET'
cfg['deploy']['repo_path'] = '$REPO_PATH'
cfg['deploy']['web_root'] = '$WEB_ROOT'
cfg['deploy']['branch'] = 'main'
with open('/etc/novacpx/config.ini', 'w') as f: cfg.write(f)
print('Config updated')
PYEOF
# Install deploy runner
cp "$REPO_PATH/deploy/deploy-runner.sh" /usr/local/bin/novacpx-deploy
chmod +x /usr/local/bin/novacpx-deploy
# Install webhook handler into web root
mkdir -p "$WEB_ROOT/deploy"
cp "$REPO_PATH/deploy/webhook.php" "$WEB_ROOT/deploy/webhook.php"
chown www-data:www-data "$WEB_ROOT/deploy/webhook.php"
# Add cron job (every minute)
(crontab -l 2>/dev/null | grep -v novacpx-deploy; echo "* * * * * root /usr/local/bin/novacpx-deploy >> /var/log/novacpx/deploy.log 2>&1") | crontab -
echo ""
echo "Auto-deploy configured!"
echo "Webhook URL: https://$(hostname -I | awk '{print $1}'):2083/deploy/webhook.php"
echo "Webhook Secret: $SECRET"
echo ""
echo "Add this webhook to GitHub repo settings:"
echo " Repo: https://github.com/myronblair/novacpx"
echo " URL: https://$(hostname -I | awk '{print $1}'):2083/deploy/webhook.php"
echo " Content-Type: application/json"
echo " Secret: $SECRET"
echo " Events: Push"
+56
View File
@@ -0,0 +1,56 @@
<?php
/**
* NovaCPX Auto-Deploy Webhook Handler
* Place at: https://<panel-ip>:2083/deploy/webhook.php
* GitHub webhook: push to main branch → this endpoint
* Secret set in /etc/novacpx/config.ini [deploy] webhook_secret
*/
$configFile = '/etc/novacpx/config.ini';
$cfg = is_file($configFile) ? parse_ini_file($configFile, true) : [];
$secret = $cfg['deploy']['webhook_secret'] ?? '';
$repoPath = $cfg['deploy']['repo_path'] ?? '/opt/novacpx-src';
$webRoot = $cfg['deploy']['web_root'] ?? '/srv/novacpx/public';
$branch = $cfg['deploy']['branch'] ?? 'main';
$logFile = '/var/log/novacpx/deploy.log';
header('Content-Type: application/json');
function log_deploy(string $msg): void {
global $logFile;
file_put_contents($logFile, date('[Y-m-d H:i:s] ') . $msg . "\n", FILE_APPEND | LOCK_EX);
}
// Verify HMAC signature
$rawBody = file_get_contents('php://input');
$hubSig = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? '';
if ($secret) {
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
if (!hash_equals($expected, $hubSig)) {
http_response_code(403);
log_deploy('BLOCKED: invalid signature');
echo json_encode(['error' => 'Invalid signature']);
exit;
}
}
$payload = json_decode($rawBody, true);
$pushedBranch = basename($payload['ref'] ?? '');
if ($pushedBranch !== $branch) {
echo json_encode(['status' => 'skipped', 'reason' => "Not target branch ($branch)"]);
exit;
}
$commit = $payload['after'] ?? 'unknown';
$pusher = $payload['pusher']['name'] ?? 'unknown';
$message = $payload['head_commit']['message'] ?? '';
log_deploy("Deploy triggered by $pusher | commit $commit | $message");
// Queue the deploy (non-blocking)
$queueFile = '/tmp/novacpx-deploy-queue.txt';
file_put_contents($queueFile, "$repoPath|$webRoot|$commit\n", FILE_APPEND | LOCK_EX);
http_response_code(200);
echo json_encode(['status' => 'queued', 'commit' => $commit]);