mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-04 22:13:21 +00:00
Compare commits
1 Commits
v1.2.8
...
renovate/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15b2023668 |
@@ -43,7 +43,7 @@ PatchMon provides centralized patch management across diverse server environment
|
|||||||
|
|
||||||
### API & Integrations
|
### API & Integrations
|
||||||
- REST API under `/api/v1` with JWT auth
|
- REST API under `/api/v1` with JWT auth
|
||||||
- Proxmox LXC Auto-Enrollment - Automatically discover and enroll LXC containers from Proxmox hosts
|
- **Proxmox LXC Auto-Enrollment** - Automatically discover and enroll LXC containers from Proxmox hosts ([Documentation](PROXMOX_AUTO_ENROLLMENT.md))
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
- Rate limiting for general, auth, and agent endpoints
|
- Rate limiting for general, auth, and agent endpoints
|
||||||
@@ -85,16 +85,11 @@ apt-get upgrade -y
|
|||||||
apt install curl -y
|
apt install curl -y
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Install Script
|
#### Script
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL -o setup.sh https://raw.githubusercontent.com/PatchMon/PatchMon/refs/heads/main/setup.sh && chmod +x setup.sh && bash setup.sh
|
curl -fsSL -o setup.sh https://raw.githubusercontent.com/PatchMon/PatchMon/refs/heads/main/setup.sh && chmod +x setup.sh && bash setup.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Update Script (--update flag)
|
|
||||||
```bash
|
|
||||||
curl -fsSL -o setup.sh https://raw.githubusercontent.com/PatchMon/PatchMon/refs/heads/main/setup.sh && chmod +x setup.sh && bash setup.sh --update
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Minimum specs for building : #####
|
#### Minimum specs for building : #####
|
||||||
CPU : 2 vCPU
|
CPU : 2 vCPU
|
||||||
RAM : 2GB
|
RAM : 2GB
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "patchmon-backend",
|
"name": "patchmon-backend",
|
||||||
"version": "1.2.8",
|
"version": "1.2.7",
|
||||||
"description": "Backend API for Linux Patch Monitoring System",
|
"description": "Backend API for Linux Patch Monitoring System",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "src/server.js",
|
"main": "src/server.js",
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ const router = express.Router();
|
|||||||
function getCurrentVersion() {
|
function getCurrentVersion() {
|
||||||
try {
|
try {
|
||||||
const packageJson = require("../../package.json");
|
const packageJson = require("../../package.json");
|
||||||
return packageJson?.version || "1.2.8";
|
return packageJson?.version || "1.2.7";
|
||||||
} catch (packageError) {
|
} catch (packageError) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"Could not read version from package.json, using fallback:",
|
"Could not read version from package.json, using fallback:",
|
||||||
packageError.message,
|
packageError.message,
|
||||||
);
|
);
|
||||||
return "1.2.8";
|
return "1.2.7";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,61 +126,43 @@ async function getLatestCommit(owner, repo) {
|
|||||||
|
|
||||||
// Helper function to get commit count difference
|
// Helper function to get commit count difference
|
||||||
async function getCommitDifference(owner, repo, currentVersion) {
|
async function getCommitDifference(owner, repo, currentVersion) {
|
||||||
// Try both with and without 'v' prefix for compatibility
|
try {
|
||||||
const versionTags = [
|
const currentVersionTag = `v${currentVersion}`;
|
||||||
currentVersion, // Try without 'v' first (new format)
|
// Compare main branch with the released version tag
|
||||||
`v${currentVersion}`, // Try with 'v' prefix (old format)
|
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/compare/${currentVersionTag}...main`;
|
||||||
];
|
|
||||||
|
|
||||||
for (const versionTag of versionTags) {
|
const response = await fetch(apiUrl, {
|
||||||
try {
|
method: "GET",
|
||||||
// Compare main branch with the released version tag
|
headers: {
|
||||||
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/compare/${versionTag}...main`;
|
Accept: "application/vnd.github.v3+json",
|
||||||
|
"User-Agent": `PatchMon-Server/${getCurrentVersion()}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const response = await fetch(apiUrl, {
|
if (!response.ok) {
|
||||||
method: "GET",
|
const errorText = await response.text();
|
||||||
headers: {
|
if (
|
||||||
Accept: "application/vnd.github.v3+json",
|
errorText.includes("rate limit") ||
|
||||||
"User-Agent": `PatchMon-Server/${getCurrentVersion()}`,
|
errorText.includes("API rate limit")
|
||||||
},
|
) {
|
||||||
});
|
throw new Error("GitHub API rate limit exceeded");
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
if (
|
|
||||||
errorText.includes("rate limit") ||
|
|
||||||
errorText.includes("API rate limit")
|
|
||||||
) {
|
|
||||||
throw new Error("GitHub API rate limit exceeded");
|
|
||||||
}
|
|
||||||
// If 404, try next tag format
|
|
||||||
if (response.status === 404) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
throw new Error(
|
|
||||||
`GitHub API error: ${response.status} ${response.statusText}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const compareData = await response.json();
|
|
||||||
return {
|
|
||||||
commitsBehind: compareData.behind_by || 0, // How many commits main is behind release
|
|
||||||
commitsAhead: compareData.ahead_by || 0, // How many commits main is ahead of release
|
|
||||||
totalCommits: compareData.total_commits || 0,
|
|
||||||
branchInfo: "main branch vs release",
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
// If rate limit, throw immediately
|
|
||||||
if (error.message.includes("rate limit")) {
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
throw new Error(
|
||||||
|
`GitHub API error: ${response.status} ${response.statusText}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If all attempts failed, throw error
|
const compareData = await response.json();
|
||||||
throw new Error(
|
return {
|
||||||
`Could not find tag '${currentVersion}' or 'v${currentVersion}' in repository`,
|
commitsBehind: compareData.behind_by || 0, // How many commits main is behind release
|
||||||
);
|
commitsAhead: compareData.ahead_by || 0, // How many commits main is ahead of release
|
||||||
|
totalCommits: compareData.total_commits || 0,
|
||||||
|
branchInfo: "main branch vs release",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching commit difference:", error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to compare version strings (semantic versioning)
|
// Helper function to compare version strings (semantic versioning)
|
||||||
@@ -292,11 +274,11 @@ router.get(
|
|||||||
) {
|
) {
|
||||||
console.log("GitHub API rate limited, providing fallback data");
|
console.log("GitHub API rate limited, providing fallback data");
|
||||||
latestRelease = {
|
latestRelease = {
|
||||||
tagName: "1.2.8",
|
tagName: "v1.2.7",
|
||||||
version: "1.2.8",
|
version: "1.2.7",
|
||||||
publishedAt: "2025-10-02T17:12:53Z",
|
publishedAt: "2025-10-02T17:12:53Z",
|
||||||
htmlUrl:
|
htmlUrl:
|
||||||
"https://github.com/PatchMon/PatchMon/releases/tag/1.2.8",
|
"https://github.com/PatchMon/PatchMon/releases/tag/v1.2.7",
|
||||||
};
|
};
|
||||||
latestCommit = {
|
latestCommit = {
|
||||||
sha: "cc89df161b8ea5d48ff95b0eb405fe69042052cd",
|
sha: "cc89df161b8ea5d48ff95b0eb405fe69042052cd",
|
||||||
@@ -314,14 +296,10 @@ router.get(
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Fall back to cached data for other errors
|
// Fall back to cached data for other errors
|
||||||
const githubRepoUrl = settings.githubRepoUrl || DEFAULT_GITHUB_REPO;
|
|
||||||
latestRelease = settings.latest_version
|
latestRelease = settings.latest_version
|
||||||
? {
|
? {
|
||||||
version: settings.latest_version,
|
version: settings.latest_version,
|
||||||
tagName: settings.latest_version,
|
tagName: `v${settings.latest_version}`,
|
||||||
publishedAt: null, // Only use date from GitHub API, not cached data
|
|
||||||
// Note: URL may need 'v' prefix depending on actual tag format in repo
|
|
||||||
htmlUrl: `${githubRepoUrl.replace(/\.git$/, "")}/releases/tag/${settings.latest_version}`,
|
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ class UpdateScheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read version from package.json dynamically
|
// Read version from package.json dynamically
|
||||||
let currentVersion = "1.2.8"; // fallback
|
let currentVersion = "1.2.7"; // fallback
|
||||||
try {
|
try {
|
||||||
const packageJson = require("../../package.json");
|
const packageJson = require("../../package.json");
|
||||||
if (packageJson?.version) {
|
if (packageJson?.version) {
|
||||||
@@ -214,7 +214,7 @@ class UpdateScheduler {
|
|||||||
const httpsRepoUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
|
const httpsRepoUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
|
||||||
|
|
||||||
// Get current version for User-Agent
|
// Get current version for User-Agent
|
||||||
let currentVersion = "1.2.8"; // fallback
|
let currentVersion = "1.2.7"; // fallback
|
||||||
try {
|
try {
|
||||||
const packageJson = require("../../package.json");
|
const packageJson = require("../../package.json");
|
||||||
if (packageJson?.version) {
|
if (packageJson?.version) {
|
||||||
|
|||||||
@@ -8,94 +8,19 @@ log() {
|
|||||||
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" >&2
|
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" >&2
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to extract version from agent script
|
# Copy files from agents_backup to agents if agents directory is empty and no .sh files are present
|
||||||
get_agent_version() {
|
if [ -d "/app/agents" ] && [ -z "$(find /app/agents -maxdepth 1 -type f -name '*.sh' | head -n 1)" ]; then
|
||||||
local file="$1"
|
if [ -d "/app/agents_backup" ]; then
|
||||||
if [ -f "$file" ]; then
|
log "Agents directory is empty, copying from backup..."
|
||||||
grep -m 1 '^AGENT_VERSION=' "$file" | cut -d'"' -f2 2>/dev/null || echo "0.0.0"
|
|
||||||
else
|
|
||||||
echo "0.0.0"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to compare versions (returns 0 if $1 > $2)
|
|
||||||
version_greater() {
|
|
||||||
# Use sort -V for version comparison
|
|
||||||
test "$(printf '%s\n' "$1" "$2" | sort -V | tail -n1)" = "$1" && test "$1" != "$2"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check and update agent files if necessary
|
|
||||||
update_agents() {
|
|
||||||
local backup_agent="/app/agents_backup/patchmon-agent.sh"
|
|
||||||
local current_agent="/app/agents/patchmon-agent.sh"
|
|
||||||
|
|
||||||
# Check if agents directory exists
|
|
||||||
if [ ! -d "/app/agents" ]; then
|
|
||||||
log "ERROR: /app/agents directory not found"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if backup exists
|
|
||||||
if [ ! -d "/app/agents_backup" ]; then
|
|
||||||
log "WARNING: agents_backup directory not found, skipping agent update"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Get versions
|
|
||||||
local backup_version=$(get_agent_version "$backup_agent")
|
|
||||||
local current_version=$(get_agent_version "$current_agent")
|
|
||||||
|
|
||||||
log "Agent version check:"
|
|
||||||
log " Image version: ${backup_version}"
|
|
||||||
log " Volume version: ${current_version}"
|
|
||||||
|
|
||||||
# Determine if update is needed
|
|
||||||
local needs_update=0
|
|
||||||
|
|
||||||
# Case 1: No agents in volume (first time setup)
|
|
||||||
if [ -z "$(find /app/agents -maxdepth 1 -type f -name '*.sh' 2>/dev/null | head -n 1)" ]; then
|
|
||||||
log "Agents directory is empty - performing initial copy"
|
|
||||||
needs_update=1
|
|
||||||
# Case 2: Backup version is newer
|
|
||||||
elif version_greater "$backup_version" "$current_version"; then
|
|
||||||
log "Newer agent version available (${backup_version} > ${current_version})"
|
|
||||||
needs_update=1
|
|
||||||
else
|
|
||||||
log "Agents are up to date"
|
|
||||||
needs_update=0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Perform update if needed
|
|
||||||
if [ $needs_update -eq 1 ]; then
|
|
||||||
log "Updating agents to version ${backup_version}..."
|
|
||||||
|
|
||||||
# Create backup of existing agents if they exist
|
|
||||||
if [ -f "$current_agent" ]; then
|
|
||||||
local backup_timestamp=$(date +%Y%m%d_%H%M%S)
|
|
||||||
local backup_name="/app/agents/patchmon-agent.sh.backup.${backup_timestamp}"
|
|
||||||
cp "$current_agent" "$backup_name" 2>/dev/null || true
|
|
||||||
log "Previous agent backed up to: $(basename $backup_name)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Copy new agents
|
|
||||||
cp -r /app/agents_backup/* /app/agents/
|
cp -r /app/agents_backup/* /app/agents/
|
||||||
|
else
|
||||||
# Verify update
|
log "Warning: agents_backup directory not found"
|
||||||
local new_version=$(get_agent_version "$current_agent")
|
|
||||||
if [ "$new_version" = "$backup_version" ]; then
|
|
||||||
log "✅ Agents successfully updated to version ${new_version}"
|
|
||||||
else
|
|
||||||
log "⚠️ Warning: Agent update may have failed (expected: ${backup_version}, got: ${new_version})"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
}
|
else
|
||||||
|
log "Agents directory already contains files, skipping copy"
|
||||||
|
fi
|
||||||
|
|
||||||
# Main execution
|
log "Starting PatchMon Backend (${NODE_ENV:-production})..."
|
||||||
log "PatchMon Backend Container Starting..."
|
|
||||||
log "Environment: ${NODE_ENV:-production}"
|
|
||||||
|
|
||||||
# Update agents (version-aware)
|
|
||||||
update_agents
|
|
||||||
|
|
||||||
log "Running database migrations..."
|
log "Running database migrations..."
|
||||||
npx prisma migrate deploy
|
npx prisma migrate deploy
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: patchmon-dev
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
database:
|
database:
|
||||||
image: postgres:17-alpine
|
image: postgres:18-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: patchmon_db
|
POSTGRES_DB: patchmon_db
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: patchmon
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
database:
|
database:
|
||||||
image: postgres:17-alpine
|
image: postgres:18-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: patchmon_db
|
POSTGRES_DB: patchmon_db
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "patchmon-frontend",
|
"name": "patchmon-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.2.8",
|
"version": "1.2.7",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -128,14 +128,12 @@ const VersionUpdateTab = () => {
|
|||||||
<span className="text-lg font-mono text-secondary-900 dark:text-white">
|
<span className="text-lg font-mono text-secondary-900 dark:text-white">
|
||||||
{versionInfo.github.latestRelease.tagName}
|
{versionInfo.github.latestRelease.tagName}
|
||||||
</span>
|
</span>
|
||||||
{versionInfo.github.latestRelease.publishedAt && (
|
<div className="text-xs text-secondary-500 dark:text-secondary-400">
|
||||||
<div className="text-xs text-secondary-500 dark:text-secondary-400">
|
Published:{" "}
|
||||||
Published:{" "}
|
{new Date(
|
||||||
{new Date(
|
versionInfo.github.latestRelease.publishedAt,
|
||||||
versionInfo.github.latestRelease.publishedAt,
|
).toLocaleDateString()}
|
||||||
).toLocaleDateString()}
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "patchmon",
|
"name": "patchmon",
|
||||||
"version": "1.2.8",
|
"version": "1.2.7",
|
||||||
"description": "Linux Patch Monitoring System",
|
"description": "Linux Patch Monitoring System",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
372
setup.sh
372
setup.sh
@@ -34,7 +34,7 @@ BLUE='\033[0;34m'
|
|||||||
NC='\033[0m' # No Color
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
# Global variables
|
# Global variables
|
||||||
SCRIPT_VERSION="self-hosting-install.sh v1.2.8-selfhost-2025-10-10-6"
|
SCRIPT_VERSION="self-hosting-install.sh v1.2.7-selfhost-2025-01-20-1"
|
||||||
DEFAULT_GITHUB_REPO="https://github.com/PatchMon/PatchMon.git"
|
DEFAULT_GITHUB_REPO="https://github.com/PatchMon/PatchMon.git"
|
||||||
FQDN=""
|
FQDN=""
|
||||||
CUSTOM_FQDN=""
|
CUSTOM_FQDN=""
|
||||||
@@ -60,9 +60,6 @@ SERVICE_USE_LETSENCRYPT="true" # Will be set based on user input
|
|||||||
SERVER_PROTOCOL_SEL="https"
|
SERVER_PROTOCOL_SEL="https"
|
||||||
SERVER_PORT_SEL="" # Will be set to BACKEND_PORT in init_instance_vars
|
SERVER_PORT_SEL="" # Will be set to BACKEND_PORT in init_instance_vars
|
||||||
SETUP_NGINX="true"
|
SETUP_NGINX="true"
|
||||||
UPDATE_MODE="false"
|
|
||||||
SELECTED_INSTANCE=""
|
|
||||||
SELECTED_SERVICE_NAME=""
|
|
||||||
|
|
||||||
# Functions
|
# Functions
|
||||||
print_status() {
|
print_status() {
|
||||||
@@ -645,61 +642,31 @@ EOF
|
|||||||
|
|
||||||
# Setup database for instance
|
# Setup database for instance
|
||||||
setup_database() {
|
setup_database() {
|
||||||
print_info "Setting up database: $DB_NAME"
|
print_info "Creating database: $DB_NAME"
|
||||||
|
|
||||||
# Check if sudo is available for user switching
|
# Check if sudo is available for user switching
|
||||||
if command -v sudo >/dev/null 2>&1; then
|
if command -v sudo >/dev/null 2>&1; then
|
||||||
# Check if user exists
|
# Drop and recreate database and user for clean state
|
||||||
user_exists=$(sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'" || echo "0")
|
sudo -u postgres psql -c "DROP DATABASE IF EXISTS $DB_NAME;" || true
|
||||||
|
sudo -u postgres psql -c "DROP USER IF EXISTS $DB_USER;" || true
|
||||||
|
|
||||||
if [ "$user_exists" = "1" ]; then
|
# Create database and user
|
||||||
print_info "Database user $DB_USER already exists, skipping creation"
|
sudo -u postgres psql -c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';"
|
||||||
else
|
sudo -u postgres psql -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;"
|
||||||
print_info "Creating database user $DB_USER"
|
|
||||||
sudo -u postgres psql -c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if database exists
|
|
||||||
db_exists=$(sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='$DB_NAME'" || echo "0")
|
|
||||||
|
|
||||||
if [ "$db_exists" = "1" ]; then
|
|
||||||
print_info "Database $DB_NAME already exists, skipping creation"
|
|
||||||
else
|
|
||||||
print_info "Creating database $DB_NAME"
|
|
||||||
sudo -u postgres psql -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Always grant privileges (in case they were revoked)
|
|
||||||
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;"
|
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;"
|
||||||
else
|
else
|
||||||
# Alternative method for systems without sudo (run as postgres user directly)
|
# Alternative method for systems without sudo (run as postgres user directly)
|
||||||
print_warning "sudo not available, using alternative method for PostgreSQL setup"
|
print_warning "sudo not available, using alternative method for PostgreSQL setup"
|
||||||
|
|
||||||
# Check if user exists
|
# Switch to postgres user using su
|
||||||
user_exists=$(su - postgres -c "psql -tAc \"SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'\"" || echo "0")
|
su - postgres -c "psql -c \"DROP DATABASE IF EXISTS $DB_NAME;\"" || true
|
||||||
|
su - postgres -c "psql -c \"DROP USER IF EXISTS $DB_USER;\"" || true
|
||||||
if [ "$user_exists" = "1" ]; then
|
su - postgres -c "psql -c \"CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';\""
|
||||||
print_info "Database user $DB_USER already exists, skipping creation"
|
su - postgres -c "psql -c \"CREATE DATABASE $DB_NAME OWNER $DB_USER;\""
|
||||||
else
|
|
||||||
print_info "Creating database user $DB_USER"
|
|
||||||
su - postgres -c "psql -c \"CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';\""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if database exists
|
|
||||||
db_exists=$(su - postgres -c "psql -tAc \"SELECT 1 FROM pg_database WHERE datname='$DB_NAME'\"" || echo "0")
|
|
||||||
|
|
||||||
if [ "$db_exists" = "1" ]; then
|
|
||||||
print_info "Database $DB_NAME already exists, skipping creation"
|
|
||||||
else
|
|
||||||
print_info "Creating database $DB_NAME"
|
|
||||||
su - postgres -c "psql -c \"CREATE DATABASE $DB_NAME OWNER $DB_USER;\""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Always grant privileges (in case they were revoked)
|
|
||||||
su - postgres -c "psql -c \"GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;\""
|
su - postgres -c "psql -c \"GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;\""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
print_status "Database setup complete for $DB_NAME"
|
print_status "Database $DB_NAME created with user $DB_USER"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Clone application repository
|
# Clone application repository
|
||||||
@@ -867,7 +834,7 @@ EOF
|
|||||||
cat > frontend/.env << EOF
|
cat > frontend/.env << EOF
|
||||||
VITE_API_URL=$SERVER_PROTOCOL_SEL://$FQDN/api/v1
|
VITE_API_URL=$SERVER_PROTOCOL_SEL://$FQDN/api/v1
|
||||||
VITE_APP_NAME=PatchMon
|
VITE_APP_NAME=PatchMon
|
||||||
VITE_APP_VERSION=1.2.8
|
VITE_APP_VERSION=1.2.7
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
print_status "Environment files created"
|
print_status "Environment files created"
|
||||||
@@ -1239,7 +1206,7 @@ create_agent_version() {
|
|||||||
|
|
||||||
# Priority 2: Use fallback version if not found
|
# Priority 2: Use fallback version if not found
|
||||||
if [ "$current_version" = "N/A" ] || [ -z "$current_version" ]; then
|
if [ "$current_version" = "N/A" ] || [ -z "$current_version" ]; then
|
||||||
current_version="1.2.8"
|
current_version="1.2.7"
|
||||||
print_warning "Could not determine version, using fallback: $current_version"
|
print_warning "Could not determine version, using fallback: $current_version"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -1583,287 +1550,11 @@ deploy_instance() {
|
|||||||
:
|
:
|
||||||
}
|
}
|
||||||
|
|
||||||
# Detect existing PatchMon installations
|
|
||||||
detect_installations() {
|
|
||||||
local installations=()
|
|
||||||
|
|
||||||
# Find all directories in /opt that contain PatchMon installations
|
|
||||||
if [ -d "/opt" ]; then
|
|
||||||
for dir in /opt/*/; do
|
|
||||||
local dirname=$(basename "$dir")
|
|
||||||
# Skip backup directories
|
|
||||||
if [[ "$dirname" =~ \.backup\. ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
# Check if it's a PatchMon installation
|
|
||||||
if [ -f "$dir/backend/package.json" ] && grep -q "patchmon" "$dir/backend/package.json" 2>/dev/null; then
|
|
||||||
installations+=("$dirname")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "${installations[@]}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Select installation to update
|
|
||||||
select_installation_to_update() {
|
|
||||||
local installations=($(detect_installations))
|
|
||||||
|
|
||||||
if [ ${#installations[@]} -eq 0 ]; then
|
|
||||||
print_error "No existing PatchMon installations found in /opt"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_info "Found ${#installations[@]} existing installation(s):"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
local i=1
|
|
||||||
declare -A install_map
|
|
||||||
for install in "${installations[@]}"; do
|
|
||||||
# Get current version if possible
|
|
||||||
local version="unknown"
|
|
||||||
if [ -f "/opt/$install/backend/package.json" ]; then
|
|
||||||
version=$(grep '"version"' "/opt/$install/backend/package.json" | head -1 | sed 's/.*"version": "\([^"]*\)".*/\1/')
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Get service status - try multiple naming conventions
|
|
||||||
# Convention 1: Just the install name (e.g., patchmon.internal)
|
|
||||||
local service_name="$install"
|
|
||||||
# Convention 2: patchmon. prefix (e.g., patchmon.patchmon.internal)
|
|
||||||
local alt_service_name1="patchmon.$install"
|
|
||||||
# Convention 3: patchmon- prefix with underscores (e.g., patchmon-patchmon_internal)
|
|
||||||
local alt_service_name2="patchmon-$(echo "$install" | tr '.' '_')"
|
|
||||||
local status="unknown"
|
|
||||||
|
|
||||||
# Try convention 1 first (most common)
|
|
||||||
if systemctl is-active --quiet "$service_name" 2>/dev/null; then
|
|
||||||
status="running"
|
|
||||||
elif systemctl is-enabled --quiet "$service_name" 2>/dev/null; then
|
|
||||||
status="stopped"
|
|
||||||
# Try convention 2
|
|
||||||
elif systemctl is-active --quiet "$alt_service_name1" 2>/dev/null; then
|
|
||||||
status="running"
|
|
||||||
service_name="$alt_service_name1"
|
|
||||||
elif systemctl is-enabled --quiet "$alt_service_name1" 2>/dev/null; then
|
|
||||||
status="stopped"
|
|
||||||
service_name="$alt_service_name1"
|
|
||||||
# Try convention 3
|
|
||||||
elif systemctl is-active --quiet "$alt_service_name2" 2>/dev/null; then
|
|
||||||
status="running"
|
|
||||||
service_name="$alt_service_name2"
|
|
||||||
elif systemctl is-enabled --quiet "$alt_service_name2" 2>/dev/null; then
|
|
||||||
status="stopped"
|
|
||||||
service_name="$alt_service_name2"
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf "%2d. %-30s (v%-10s - %s)\n" "$i" "$install" "$version" "$status"
|
|
||||||
install_map[$i]="$install"
|
|
||||||
# Store the service name for later use
|
|
||||||
declare -g "service_map_$i=$service_name"
|
|
||||||
i=$((i + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
read_input "Select installation number to update" SELECTION "1"
|
|
||||||
|
|
||||||
if [[ "$SELECTION" =~ ^[0-9]+$ ]] && [ -n "${install_map[$SELECTION]}" ]; then
|
|
||||||
SELECTED_INSTANCE="${install_map[$SELECTION]}"
|
|
||||||
# Get the stored service name
|
|
||||||
local varname="service_map_$SELECTION"
|
|
||||||
SELECTED_SERVICE_NAME="${!varname}"
|
|
||||||
print_status "Selected: $SELECTED_INSTANCE"
|
|
||||||
print_info "Service: $SELECTED_SERVICE_NAME"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
print_error "Invalid selection. Please enter a number from 1 to ${#installations[@]}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# Update existing installation
|
|
||||||
update_installation() {
|
|
||||||
local instance_dir="/opt/$SELECTED_INSTANCE"
|
|
||||||
local service_name="$SELECTED_SERVICE_NAME"
|
|
||||||
|
|
||||||
print_info "Updating PatchMon installation: $SELECTED_INSTANCE"
|
|
||||||
print_info "Installation directory: $instance_dir"
|
|
||||||
print_info "Service name: $service_name"
|
|
||||||
|
|
||||||
# Verify it's a git repository
|
|
||||||
if [ ! -d "$instance_dir/.git" ]; then
|
|
||||||
print_error "Installation directory is not a git repository"
|
|
||||||
print_error "Cannot perform git-based update"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Add git safe.directory to avoid ownership issues when running as root
|
|
||||||
print_info "Configuring git safe.directory..."
|
|
||||||
git config --global --add safe.directory "$instance_dir" 2>/dev/null || true
|
|
||||||
|
|
||||||
# Load existing .env to get database credentials
|
|
||||||
if [ -f "$instance_dir/backend/.env" ]; then
|
|
||||||
source "$instance_dir/backend/.env"
|
|
||||||
print_status "Loaded existing configuration"
|
|
||||||
|
|
||||||
# Parse DATABASE_URL to extract credentials
|
|
||||||
# Format: postgresql://user:password@host:port/database
|
|
||||||
if [ -n "$DATABASE_URL" ]; then
|
|
||||||
# Extract components using regex
|
|
||||||
DB_USER=$(echo "$DATABASE_URL" | sed -n 's|postgresql://\([^:]*\):.*|\1|p')
|
|
||||||
DB_PASS=$(echo "$DATABASE_URL" | sed -n 's|postgresql://[^:]*:\([^@]*\)@.*|\1|p')
|
|
||||||
DB_HOST=$(echo "$DATABASE_URL" | sed -n 's|.*@\([^:]*\):.*|\1|p')
|
|
||||||
DB_PORT=$(echo "$DATABASE_URL" | sed -n 's|.*:\([0-9]*\)/.*|\1|p')
|
|
||||||
DB_NAME=$(echo "$DATABASE_URL" | sed -n 's|.*/\([^?]*\).*|\1|p')
|
|
||||||
|
|
||||||
print_info "Database: $DB_NAME (user: $DB_USER)"
|
|
||||||
else
|
|
||||||
print_error "DATABASE_URL not found in .env file"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
print_error "Cannot find .env file at $instance_dir/backend/.env"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Select branch/version to update to
|
|
||||||
select_branch
|
|
||||||
|
|
||||||
print_info "Updating to: $DEPLOYMENT_BRANCH"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
read_yes_no "Proceed with update? This will pull new code and restart services" CONFIRM_UPDATE "y"
|
|
||||||
|
|
||||||
if [ "$CONFIRM_UPDATE" != "y" ]; then
|
|
||||||
print_warning "Update cancelled by user"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stop the service
|
|
||||||
print_info "Stopping service: $service_name"
|
|
||||||
systemctl stop "$service_name" || true
|
|
||||||
|
|
||||||
# Create backup directory
|
|
||||||
local timestamp=$(date +%Y%m%d_%H%M%S)
|
|
||||||
local backup_dir="$instance_dir.backup.$timestamp"
|
|
||||||
local db_backup_file="$backup_dir/database_backup_$timestamp.sql"
|
|
||||||
|
|
||||||
print_info "Creating backup directory: $backup_dir"
|
|
||||||
mkdir -p "$backup_dir"
|
|
||||||
|
|
||||||
# Backup database
|
|
||||||
print_info "Backing up database: $DB_NAME"
|
|
||||||
if PGPASSWORD="$DB_PASS" pg_dump -h "$DB_HOST" -U "$DB_USER" -d "$DB_NAME" -F c -f "$db_backup_file" 2>/dev/null; then
|
|
||||||
print_status "Database backup created: $db_backup_file"
|
|
||||||
else
|
|
||||||
print_warning "Database backup failed, but continuing with code backup"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Backup code
|
|
||||||
print_info "Backing up code files..."
|
|
||||||
cp -r "$instance_dir" "$backup_dir/code"
|
|
||||||
print_status "Code backup created"
|
|
||||||
|
|
||||||
# Update code
|
|
||||||
print_info "Pulling latest code from branch: $DEPLOYMENT_BRANCH"
|
|
||||||
cd "$instance_dir"
|
|
||||||
|
|
||||||
# Fetch latest changes
|
|
||||||
git fetch origin
|
|
||||||
|
|
||||||
# Checkout the selected branch/tag
|
|
||||||
git checkout "$DEPLOYMENT_BRANCH"
|
|
||||||
git pull origin "$DEPLOYMENT_BRANCH" || git pull # For tags, just pull
|
|
||||||
|
|
||||||
print_status "Code updated successfully"
|
|
||||||
|
|
||||||
# Update dependencies
|
|
||||||
print_info "Updating backend dependencies..."
|
|
||||||
cd "$instance_dir/backend"
|
|
||||||
npm install --production --ignore-scripts
|
|
||||||
|
|
||||||
print_info "Updating frontend dependencies..."
|
|
||||||
cd "$instance_dir/frontend"
|
|
||||||
npm install --ignore-scripts
|
|
||||||
|
|
||||||
# Build frontend
|
|
||||||
print_info "Building frontend..."
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Run database migrations and generate Prisma client
|
|
||||||
print_info "Running database migrations..."
|
|
||||||
cd "$instance_dir/backend"
|
|
||||||
npx prisma generate
|
|
||||||
npx prisma migrate deploy
|
|
||||||
|
|
||||||
# Start the service
|
|
||||||
print_info "Starting service: $service_name"
|
|
||||||
systemctl start "$service_name"
|
|
||||||
|
|
||||||
# Wait a moment and check status
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
if systemctl is-active --quiet "$service_name"; then
|
|
||||||
print_success "✅ Update completed successfully!"
|
|
||||||
print_status "Service $service_name is running"
|
|
||||||
|
|
||||||
# Get new version
|
|
||||||
local new_version=$(grep '"version"' "$instance_dir/backend/package.json" | head -1 | sed 's/.*"version": "\([^"]*\)".*/\1/')
|
|
||||||
print_info "Updated to version: $new_version"
|
|
||||||
echo ""
|
|
||||||
print_info "Backup Information:"
|
|
||||||
print_info " Code backup: $backup_dir/code"
|
|
||||||
print_info " Database backup: $db_backup_file"
|
|
||||||
echo ""
|
|
||||||
print_info "To restore database if needed:"
|
|
||||||
print_info " PGPASSWORD=\"$DB_PASS\" pg_restore -h \"$DB_HOST\" -U \"$DB_USER\" -d \"$DB_NAME\" -c \"$db_backup_file\""
|
|
||||||
echo ""
|
|
||||||
else
|
|
||||||
print_error "Service failed to start after update"
|
|
||||||
echo ""
|
|
||||||
print_warning "ROLLBACK INSTRUCTIONS:"
|
|
||||||
print_info "1. Restore code:"
|
|
||||||
print_info " sudo rm -rf $instance_dir"
|
|
||||||
print_info " sudo mv $backup_dir/code $instance_dir"
|
|
||||||
echo ""
|
|
||||||
print_info "2. Restore database:"
|
|
||||||
print_info " PGPASSWORD=\"$DB_PASS\" pg_restore -h \"$DB_HOST\" -U \"$DB_USER\" -d \"$DB_NAME\" -c \"$db_backup_file\""
|
|
||||||
echo ""
|
|
||||||
print_info "3. Restart service:"
|
|
||||||
print_info " sudo systemctl start $service_name"
|
|
||||||
echo ""
|
|
||||||
print_info "Check logs: journalctl -u $service_name -f"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main script execution
|
# Main script execution
|
||||||
main() {
|
main() {
|
||||||
# Parse command-line arguments
|
|
||||||
if [ "$1" = "--update" ]; then
|
|
||||||
UPDATE_MODE="true"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Log script entry
|
# Log script entry
|
||||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Script started - Update mode: $UPDATE_MODE" >> "$DEBUG_LOG"
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Interactive installation started" >> "$DEBUG_LOG"
|
||||||
|
|
||||||
# Handle update mode
|
|
||||||
if [ "$UPDATE_MODE" = "true" ]; then
|
|
||||||
print_banner
|
|
||||||
print_info "🔄 PatchMon Update Mode"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Select installation to update
|
|
||||||
select_installation_to_update
|
|
||||||
|
|
||||||
# Perform update
|
|
||||||
update_installation
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Normal installation mode
|
|
||||||
# Run interactive setup
|
# Run interactive setup
|
||||||
interactive_setup
|
interactive_setup
|
||||||
|
|
||||||
@@ -1897,30 +1588,5 @@ main() {
|
|||||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] deploy_instance function completed" >> "$DEBUG_LOG"
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] deploy_instance function completed" >> "$DEBUG_LOG"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Show usage/help
|
# Run main function (no arguments needed for interactive mode)
|
||||||
show_usage() {
|
main
|
||||||
echo "PatchMon Self-Hosting Installation & Update Script"
|
|
||||||
echo "Version: $SCRIPT_VERSION"
|
|
||||||
echo ""
|
|
||||||
echo "Usage:"
|
|
||||||
echo " $0 # Interactive installation (default)"
|
|
||||||
echo " $0 --update # Update existing installation"
|
|
||||||
echo " $0 --help # Show this help message"
|
|
||||||
echo ""
|
|
||||||
echo "Examples:"
|
|
||||||
echo " # New installation:"
|
|
||||||
echo " sudo bash $0"
|
|
||||||
echo ""
|
|
||||||
echo " # Update existing installation:"
|
|
||||||
echo " sudo bash $0 --update"
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check for help flag
|
|
||||||
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
|
||||||
show_usage
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Run main function
|
|
||||||
main "$@"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user