Compare commits
2 Commits
v1.1.8
...
release/v1
| Author | SHA1 | Date | |
|---|---|---|---|
| d6f0c23dd8 | |||
| ae15404496 |
@ -1,40 +0,0 @@
|
|||||||
# Build artifacts
|
|
||||||
target/
|
|
||||||
*.deb
|
|
||||||
package-build/
|
|
||||||
|
|
||||||
# Frontend build output (rebuilt in Docker)
|
|
||||||
frontend/dist/
|
|
||||||
frontend/node_modules/
|
|
||||||
|
|
||||||
# Git
|
|
||||||
.git/
|
|
||||||
.gitignore
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
|
|
||||||
# Docker
|
|
||||||
Dockerfile
|
|
||||||
docker-compose.yml
|
|
||||||
.dockerignore
|
|
||||||
|
|
||||||
# Environment
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
|
|
||||||
# Documentation
|
|
||||||
docs/
|
|
||||||
|
|
||||||
# Agent Zero project data
|
|
||||||
.a0proj/
|
|
||||||
|
|
||||||
# Python
|
|
||||||
venv/
|
|
||||||
__pycache__/
|
|
||||||
|
|
||||||
# Misc
|
|
||||||
*.md
|
|
||||||
!README.md
|
|
||||||
LICENSE
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
# Linux Patch Manager — Docker Environment Variables
|
|
||||||
# Copy this file to .env and edit the values before running docker compose up.
|
|
||||||
|
|
||||||
# Required: PostgreSQL password for the patch_manager user
|
|
||||||
DB_PASSWORD=changeme-to-a-strong-password
|
|
||||||
|
|
||||||
# Optional: Docker image tag (defaults to 'latest' if not set)
|
|
||||||
TAG=latest
|
|
||||||
45
.github/workflows/ci.yml
vendored
45
.github/workflows/ci.yml
vendored
@ -12,7 +12,6 @@ env:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
packages: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
rust-format:
|
rust-format:
|
||||||
@ -129,47 +128,3 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
body: ${{ steps.release_notes.outputs.notes }}
|
body: ${{ steps.release_notes.outputs.notes }}
|
||||||
files: linux-patch-manager_*.deb
|
files: linux-patch-manager_*.deb
|
||||||
|
|
||||||
docker:
|
|
||||||
name: Docker Build & Push
|
|
||||||
needs: [rust-format, clippy, rust-test, security-audit, frontend-lint]
|
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Extract Docker metadata
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: ghcr.io/draco-lunaris/linux-patch-manager
|
|
||||||
tags: |
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
|
||||||
type=semver,pattern={{major}}
|
|
||||||
type=sha
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/amd64
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -28,9 +28,6 @@ frontend/dist
|
|||||||
*.deb
|
*.deb
|
||||||
package-build/
|
package-build/
|
||||||
|
|
||||||
# Docker environment
|
|
||||||
.env
|
|
||||||
|
|
||||||
# Private key material - NEVER commit
|
# Private key material - NEVER commit
|
||||||
*.key
|
*.key
|
||||||
*.key.pem
|
*.key.pem
|
||||||
|
|||||||
@ -12,7 +12,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "1.1.8"
|
version = "1.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Echo <echo@moon-dragon.us>"]
|
authors = ["Echo <echo@moon-dragon.us>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
141
Dockerfile
141
Dockerfile
@ -1,141 +0,0 @@
|
|||||||
# =============================================================================
|
|
||||||
# Linux Patch Manager — Multi-stage Docker Build
|
|
||||||
# =============================================================================
|
|
||||||
# Build: docker build -t linux-patch-manager .
|
|
||||||
# Run: docker compose up
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Stage 1: Rust build (Ubuntu 24.04 + rustup)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
FROM ubuntu:24.04 AS rust-builder
|
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
build-essential \
|
|
||||||
curl \
|
|
||||||
pkg-config \
|
|
||||||
libssl-dev \
|
|
||||||
libfontconfig1-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install Rust via rustup (stable channel, provides 1.85+)
|
|
||||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
|
||||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
# Cache dependencies by building a dummy project first
|
|
||||||
COPY Cargo.toml Cargo.lock ./
|
|
||||||
COPY crates/pm-web/Cargo.toml crates/pm-web/Cargo.toml
|
|
||||||
COPY crates/pm-worker/Cargo.toml crates/pm-worker/Cargo.toml
|
|
||||||
COPY crates/pm-core/Cargo.toml crates/pm-core/Cargo.toml
|
|
||||||
COPY crates/pm-agent-client/Cargo.toml crates/pm-agent-client/Cargo.toml
|
|
||||||
COPY crates/pm-auth/Cargo.toml crates/pm-auth/Cargo.toml
|
|
||||||
COPY crates/pm-ca/Cargo.toml crates/pm-ca/Cargo.toml
|
|
||||||
COPY crates/pm-reports/Cargo.toml crates/pm-reports/Cargo.toml
|
|
||||||
COPY crates/migrate-secrets/Cargo.toml crates/migrate-secrets/Cargo.toml
|
|
||||||
RUN mkdir -p crates/pm-web/src crates/pm-worker/src crates/pm-core/src \
|
|
||||||
crates/pm-agent-client/src crates/pm-auth/src crates/pm-ca/src \
|
|
||||||
crates/pm-reports/src crates/migrate-secrets/src
|
|
||||||
RUN echo 'fn main(){}' > crates/pm-web/src/main.rs \
|
|
||||||
&& echo 'fn main(){}' > crates/pm-worker/src/main.rs \
|
|
||||||
&& echo '' > crates/pm-core/src/lib.rs \
|
|
||||||
&& echo '' > crates/pm-agent-client/src/lib.rs \
|
|
||||||
&& echo '' > crates/pm-auth/src/lib.rs \
|
|
||||||
&& echo '' > crates/pm-ca/src/lib.rs \
|
|
||||||
&& echo '' > crates/pm-reports/src/lib.rs \
|
|
||||||
&& echo 'fn main(){}' > crates/migrate-secrets/src/main.rs
|
|
||||||
RUN cargo build --release 2>/dev/null || true
|
|
||||||
|
|
||||||
# Now build the real project
|
|
||||||
COPY crates/ crates/
|
|
||||||
COPY migrations/ migrations/
|
|
||||||
RUN cargo build --release
|
|
||||||
|
|
||||||
# Verify binaries exist
|
|
||||||
RUN ls -la target/release/pm-web target/release/pm-worker
|
|
||||||
|
|
||||||
# Strip debug symbols
|
|
||||||
RUN strip target/release/pm-web target/release/pm-worker
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Stage 2: Frontend build (Ubuntu 24.04 + Node.js 20)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
FROM ubuntu:24.04 AS frontend-builder
|
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
curl \
|
|
||||||
ca-certificates \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install Node.js 20 via NodeSource
|
|
||||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
|
||||||
&& apt-get install -y nodejs \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app/frontend
|
|
||||||
COPY frontend/package.json frontend/package-lock.json ./
|
|
||||||
RUN npm ci --production=false
|
|
||||||
|
|
||||||
COPY frontend/ ./
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Stage 3: Runtime
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
FROM ubuntu:24.04 AS runtime
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
ca-certificates \
|
|
||||||
libssl3t64 \
|
|
||||||
libfontconfig1 \
|
|
||||||
openssl \
|
|
||||||
postgresql-client-16 \
|
|
||||||
argon2 \
|
|
||||||
curl \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Create service user
|
|
||||||
RUN useradd --system --no-create-home --shell /usr/sbin/nologin \
|
|
||||||
--comment "Linux Patch Manager service account" patch-manager
|
|
||||||
|
|
||||||
# Create directories
|
|
||||||
RUN mkdir -p /etc/patch-manager/ca /etc/patch-manager/certs \
|
|
||||||
/etc/patch-manager/jwt /etc/patch-manager/tls \
|
|
||||||
/var/log/patch-manager /opt/patch-manager \
|
|
||||||
/usr/share/patch-manager/frontend \
|
|
||||||
/usr/share/patch-manager/migrations
|
|
||||||
|
|
||||||
# Copy binaries
|
|
||||||
COPY --from=rust-builder /usr/src/app/target/release/pm-web /usr/local/bin/pm-web
|
|
||||||
COPY --from=rust-builder /usr/src/app/target/release/pm-worker /usr/local/bin/pm-worker
|
|
||||||
|
|
||||||
# Copy frontend
|
|
||||||
COPY --from=frontend-builder /usr/src/app/frontend/dist/ /usr/share/patch-manager/frontend/
|
|
||||||
|
|
||||||
# Copy migrations
|
|
||||||
COPY migrations/ /usr/share/patch-manager/migrations/
|
|
||||||
|
|
||||||
# Copy entrypoint
|
|
||||||
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
|
||||||
RUN chmod 755 /usr/local/bin/entrypoint.sh
|
|
||||||
|
|
||||||
# Copy config template
|
|
||||||
COPY config/config.example.toml /usr/share/patch-manager/config.example.toml
|
|
||||||
|
|
||||||
# Set ownership
|
|
||||||
RUN chown -R patch-manager:patch-manager \
|
|
||||||
/etc/patch-manager /var/log/patch-manager \
|
|
||||||
/opt/patch-manager /usr/share/patch-manager
|
|
||||||
|
|
||||||
# Expose HTTPS port
|
|
||||||
EXPOSE 443
|
|
||||||
|
|
||||||
# Volume for persistent data
|
|
||||||
VOLUME ["/etc/patch-manager", "/var/log/patch-manager", "/opt/patch-manager"]
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
|
||||||
48
debian/changelog
vendored
48
debian/changelog
vendored
@ -1,51 +1,3 @@
|
|||||||
linux-patch-manager (1.1.8-1) unstable; urgency=low
|
|
||||||
|
|
||||||
* Release v1.1.8
|
|
||||||
|
|
||||||
-- git-echo <git-echo@moon-dragon.us> Tue, 09 Jun 2026 11:47:58 -0500
|
|
||||||
|
|
||||||
linux-patch-manager (1.1.7-1) unstable; urgency=low
|
|
||||||
|
|
||||||
* Release v1.1.7
|
|
||||||
|
|
||||||
-- git-echo <git-echo@moon-dragon.us> Tue, 09 Jun 2026 09:11:11 -0500
|
|
||||||
|
|
||||||
linux-patch-manager (1.1.6-1) unstable; urgency=low
|
|
||||||
|
|
||||||
* Release v1.1.6
|
|
||||||
|
|
||||||
-- git-echo <git-echo@moon-dragon.us> Tue, 09 Jun 2026 08:10:52 -0500
|
|
||||||
|
|
||||||
linux-patch-manager (1.1.5-1) unstable; urgency=low
|
|
||||||
|
|
||||||
* Release v1.1.5
|
|
||||||
|
|
||||||
-- git-echo <git-echo@moon-dragon.us> Mon, 08 Jun 2026 20:15:50 -0500
|
|
||||||
|
|
||||||
linux-patch-manager (1.1.4-1) unstable; urgency=low
|
|
||||||
|
|
||||||
* Release v1.1.4
|
|
||||||
|
|
||||||
-- git-echo <git-echo@moon-dragon.us> Mon, 08 Jun 2026 17:30:35 -0500
|
|
||||||
|
|
||||||
linux-patch-manager (1.1.2-1) unstable; urgency=low
|
|
||||||
|
|
||||||
* Release v1.1.2
|
|
||||||
|
|
||||||
-- git-echo <git-echo@moon-dragon.us> Sun, 07 Jun 2026 21:19:18 -0500
|
|
||||||
|
|
||||||
linux-patch-manager (1.1.1-1) unstable; urgency=low
|
|
||||||
|
|
||||||
* Release v1.1.1
|
|
||||||
|
|
||||||
-- git-echo <git-echo@moon-dragon.us> Sun, 07 Jun 2026 18:55:59 -0500
|
|
||||||
|
|
||||||
linux-patch-manager (1.1.0-1) unstable; urgency=low
|
|
||||||
|
|
||||||
* Release v1.1.0
|
|
||||||
|
|
||||||
-- git-echo <git-echo@moon-dragon.us> Sun, 07 Jun 2026 16:47:03 -0500
|
|
||||||
|
|
||||||
linux-patch-manager (1.0.0-1) unstable; urgency=low
|
linux-patch-manager (1.0.0-1) unstable; urgency=low
|
||||||
|
|
||||||
* Release v1.0.0
|
* Release v1.0.0
|
||||||
|
|||||||
5
debian/control
vendored
5
debian/control
vendored
@ -1,10 +1,9 @@
|
|||||||
Package: linux-patch-manager
|
Package: linux-patch-manager
|
||||||
Version: 1.1.8-1
|
Version: 1.0.0-1
|
||||||
Architecture: amd64
|
Architecture: amd64
|
||||||
Maintainer: Moon Dragon <echo@moon-dragon.us>
|
Maintainer: Moon Dragon <echo@moon-dragon.us>
|
||||||
Installed-Size: 45000
|
Installed-Size: 45000
|
||||||
Pre-Depends: postgresql-16
|
Depends: postgresql-16, libssl3, libc6 (>= 2.39), libfontconfig1
|
||||||
Depends: postgresql-16, argon2, libssl3, libc6 (>= 2.39), libfontconfig1
|
|
||||||
Recommends: postgresql-client-16, fonts-dejavu-core
|
Recommends: postgresql-client-16, fonts-dejavu-core
|
||||||
Suggests: gpg
|
Suggests: gpg
|
||||||
Section: admin
|
Section: admin
|
||||||
|
|||||||
460
debian/postinst
vendored
460
debian/postinst
vendored
@ -4,391 +4,91 @@ set -e
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Linux Patch Manager — Post-install script
|
# Linux Patch Manager — Post-install script
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Fully automated: apt install ./linux-patch-manager_X.X.X-1_amd64.deb
|
|
||||||
# results in a running service with a printed admin password.
|
|
||||||
# All steps are idempotent (safe to re-run on upgrade).
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
CYAN='\033[0;36m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
|
||||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
|
||||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
|
||||||
|
|
||||||
DB_NAME="patch_manager"
|
|
||||||
DB_USER="patch_manager"
|
|
||||||
CONFIG_DIR="/etc/patch-manager"
|
|
||||||
MIGRATION_DIR="/usr/share/patch-manager/migrations"
|
|
||||||
ADMIN_PASSWORD_FILE="/etc/patch-manager/admin-password.txt"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# PostgreSQL helpers
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
psql_run() {
|
|
||||||
# Run SQL as the postgres superuser
|
|
||||||
sudo -u postgres psql -v ON_ERROR_STOP=1 "$@" 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
psql_run_db() {
|
|
||||||
# Run SQL against the patch_manager database
|
|
||||||
sudo -u postgres psql -v ON_ERROR_STOP=1 -d "${DB_NAME}" "$@" 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 1. Create service user (idempotent)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
create_service_user() {
|
|
||||||
if ! id patch-manager &>/dev/null; then
|
|
||||||
useradd --system --no-create-home --shell /usr/sbin/nologin \
|
|
||||||
--comment "Linux Patch Manager service account" patch-manager
|
|
||||||
info "Service user 'patch-manager' created."
|
|
||||||
else
|
|
||||||
info "Service user 'patch-manager' already exists."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 2. Create required directories (idempotent)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
create_directories() {
|
|
||||||
mkdir -p "${CONFIG_DIR}/ca" "${CONFIG_DIR}/certs" \
|
|
||||||
"${CONFIG_DIR}/jwt" "${CONFIG_DIR}/tls" \
|
|
||||||
/var/log/patch-manager /opt/patch-manager \
|
|
||||||
/var/backups/patch-manager
|
|
||||||
|
|
||||||
chown -R patch-manager:patch-manager \
|
|
||||||
"${CONFIG_DIR}" /var/log/patch-manager \
|
|
||||||
/opt/patch-manager /usr/share/patch-manager/frontend
|
|
||||||
|
|
||||||
chmod 750 "${CONFIG_DIR}/ca" "${CONFIG_DIR}/jwt"
|
|
||||||
chmod 700 /var/backups/patch-manager
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 3. Wait for PostgreSQL to be ready
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
wait_for_postgresql() {
|
|
||||||
info "Waiting for PostgreSQL to be ready..."
|
|
||||||
local retries=30
|
|
||||||
local delay=2
|
|
||||||
local i
|
|
||||||
for ((i = 1; i <= retries; i++)); do
|
|
||||||
if pg_isready -q 2>/dev/null; then
|
|
||||||
info "PostgreSQL is ready."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
warn "PostgreSQL not ready yet (attempt ${i}/${retries}), waiting ${delay}s..."
|
|
||||||
sleep "${delay}"
|
|
||||||
done
|
|
||||||
error "PostgreSQL did not become ready after $((retries * delay)) seconds."
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 4. Create PostgreSQL user and database (idempotent)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
setup_database() {
|
|
||||||
info "Setting up PostgreSQL database and user..."
|
|
||||||
|
|
||||||
# Generate a random password for the DB user
|
|
||||||
local db_password
|
|
||||||
db_password=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 32)
|
|
||||||
|
|
||||||
# Create role if not exists
|
|
||||||
local role_exists
|
|
||||||
role_exists=$(psql_run -t -A -c "SELECT 1 FROM pg_roles WHERE rolname='${DB_USER}'" 2>/dev/null || echo "")
|
|
||||||
if [[ "${role_exists}" != "1" ]]; then
|
|
||||||
psql_run -c "CREATE ROLE ${DB_USER} LOGIN PASSWORD '${db_password}';"
|
|
||||||
info "PostgreSQL user '${DB_USER}' created."
|
|
||||||
# Store password for config generation
|
|
||||||
echo "${db_password}" > /tmp/.pm-db-password-new
|
|
||||||
else
|
|
||||||
info "PostgreSQL user '${DB_USER}' already exists."
|
|
||||||
# Recover the DB password: try from existing config, or generate new.
|
|
||||||
local config_file="${CONFIG_DIR}/config.toml"
|
|
||||||
local existing_pw=""
|
|
||||||
if [[ -f "${config_file}" ]]; then
|
|
||||||
# Extract password from URL: postgres://user:PASSWORD@host/db
|
|
||||||
# Use @localhost anchor so passwords containing @ are extracted correctly.
|
|
||||||
existing_pw=$(sed -n 's|^url = "postgres://[^:]*:\(.*\)@localhost.*"|\1|p' "${config_file}" | head -1)
|
|
||||||
fi
|
|
||||||
if [[ -n "${existing_pw}" && "${existing_pw}" != "CHANGEME" ]]; then
|
|
||||||
# Config has a real password — sync it to PostgreSQL so the app can connect.
|
|
||||||
psql_run -c "ALTER ROLE ${DB_USER} WITH PASSWORD '${existing_pw}';" 2>/dev/null || true
|
|
||||||
echo "${existing_pw}" > /tmp/.pm-db-password-new
|
|
||||||
info "Synced DB password from existing config to PostgreSQL."
|
|
||||||
else
|
|
||||||
# No config or CHANGEME — generate a fresh password and update PostgreSQL.
|
|
||||||
db_password=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 32)
|
|
||||||
psql_run -c "ALTER ROLE ${DB_USER} WITH PASSWORD '${db_password}';" 2>/dev/null || true
|
|
||||||
echo "${db_password}" > /tmp/.pm-db-password-new
|
|
||||||
info "Generated new DB password for existing user."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create database if not exists
|
|
||||||
local db_exists
|
|
||||||
db_exists=$(psql_run -t -A -c "SELECT 1 FROM pg_database WHERE datname='${DB_NAME}'" 2>/dev/null || echo "")
|
|
||||||
if [[ "${db_exists}" != "1" ]]; then
|
|
||||||
psql_run -c "CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};"
|
|
||||||
info "Database '${DB_NAME}' created."
|
|
||||||
else
|
|
||||||
info "Database '${DB_NAME}' already exists, skipping creation."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Grant permissions (idempotent)
|
|
||||||
psql_run_db -c "GRANT USAGE ON SCHEMA public TO ${DB_USER};" 2>/dev/null || true
|
|
||||||
psql_run_db -c "GRANT CREATE ON SCHEMA public TO ${DB_USER};" 2>/dev/null || true
|
|
||||||
psql_run_db -c "GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 5. Apply database migrations (idempotent)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
apply_migrations() {
|
|
||||||
info "Applying database migrations..."
|
|
||||||
|
|
||||||
# Ensure pgcrypto extension is available
|
|
||||||
psql_run_db -c "CREATE EXTENSION IF NOT EXISTS pgcrypto;" 2>/dev/null || true
|
|
||||||
|
|
||||||
# Create migration tracking table if not exists
|
|
||||||
psql_run_db <<'MIGSQL'
|
|
||||||
CREATE TABLE IF NOT EXISTS _migrations (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
filename TEXT NOT NULL UNIQUE,
|
|
||||||
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
||||||
);
|
|
||||||
MIGSQL
|
|
||||||
|
|
||||||
# Handle upgrade from pre-migration-tracking versions:
|
|
||||||
# If tables exist but _migrations is empty, mark all existing migrations as applied.
|
|
||||||
local migration_count
|
|
||||||
migration_count=$(psql_run_db -t -A -c "SELECT COUNT(*) FROM _migrations;" 2>/dev/null || echo "0")
|
|
||||||
migration_count="${migration_count// /}"
|
|
||||||
|
|
||||||
local tables_exist
|
|
||||||
tables_exist=$(psql_run_db -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
|
|
||||||
info "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_run_db -c "INSERT INTO _migrations (filename) VALUES ('${fname}') ON CONFLICT (filename) DO NOTHING;" 2>/dev/null || true
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Apply each migration in sorted order, skipping already-applied ones
|
|
||||||
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_run_db -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
|
|
||||||
|
|
||||||
info " Applying migration: ${fname}"
|
|
||||||
if psql_run_db -f "${sql_file}"; then
|
|
||||||
psql_run_db -c "INSERT INTO _migrations (filename) VALUES ('${fname}');" 2>/dev/null || true
|
|
||||||
applied=$((applied + 1))
|
|
||||||
else
|
|
||||||
error " Failed to apply migration: ${fname}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "${applied}" -gt 0 ]]; then
|
|
||||||
info "Applied ${applied} new migration(s), skipped ${skipped} already applied."
|
|
||||||
else
|
|
||||||
info "All migrations up to date (${skipped} already applied)."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 6. Generate admin password and update database
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
generate_admin_password() {
|
|
||||||
info "Generating admin password..."
|
|
||||||
|
|
||||||
# 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, compatible with the application)
|
|
||||||
# Generate a random 16-character salt (argon2 requires minimum 8 characters)
|
|
||||||
local admin_salt
|
|
||||||
admin_salt=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 16)
|
|
||||||
local password_hash
|
|
||||||
password_hash=$(echo -n "${admin_password}" | argon2 "${admin_salt}" -id -t 3 -m 16 -p 1 -l 32 -e)
|
|
||||||
|
|
||||||
# Update admin user password in database
|
|
||||||
# Only update if the 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 SQL LIKE pattern
|
|
||||||
local placeholder_pattern
|
|
||||||
placeholder_pattern='$argon2id$v=19$m=65536,t=3,p=1$AAAAAAAAAAAAAAAA%'
|
|
||||||
|
|
||||||
local updated
|
|
||||||
updated=$(psql_run_db -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
|
|
||||||
# Write admin password to file (mode 600, owned by root)
|
|
||||||
echo "${admin_password}" > "${ADMIN_PASSWORD_FILE}"
|
|
||||||
chmod 600 "${ADMIN_PASSWORD_FILE}"
|
|
||||||
chown root:root "${ADMIN_PASSWORD_FILE}"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${CYAN}=============================================${NC}"
|
|
||||||
echo -e "${CYAN} Linux Patch Manager — Admin Credentials${NC}"
|
|
||||||
echo -e "${CYAN}=============================================${NC}"
|
|
||||||
echo -e " Username: ${GREEN}admin${NC}"
|
|
||||||
echo -e " Password: ${GREEN}${admin_password}${NC}"
|
|
||||||
echo ""
|
|
||||||
echo -e " ${YELLOW}IMPORTANT: Save this password! It will not be shown again.${NC}"
|
|
||||||
echo -e " Password also saved to: ${ADMIN_PASSWORD_FILE}"
|
|
||||||
echo -e "${CYAN}=============================================${NC}"
|
|
||||||
echo ""
|
|
||||||
else
|
|
||||||
info "Admin password already set (not a fresh install). Password file not regenerated."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 7. Write config.toml with DB URL
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Handles three scenarios:
|
|
||||||
# 1. No config file → create from example with real DB password
|
|
||||||
# 2. Config exists with CHANGEME → replace CHANGEME with real DB password
|
|
||||||
# 3. Config exists with real password → leave it alone (upgrade)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
write_config() {
|
|
||||||
local config_file="${CONFIG_DIR}/config.toml"
|
|
||||||
|
|
||||||
# Resolve the DB password to use: from setup_database() or generate fresh.
|
|
||||||
local db_password=""
|
|
||||||
if [[ -f /tmp/.pm-db-password-new ]]; then
|
|
||||||
db_password=$(cat /tmp/.pm-db-password-new)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -f "${config_file}" ]]; then
|
|
||||||
# Check if the config still has the CHANGEME placeholder
|
|
||||||
if grep -q 'CHANGEME' "${config_file}"; then
|
|
||||||
if [[ -z "${db_password}" ]]; then
|
|
||||||
# No password from setup_database() — generate a fresh one
|
|
||||||
db_password=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 32)
|
|
||||||
psql_run -c "ALTER ROLE ${DB_USER} WITH PASSWORD '${db_password}';" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
info "Replacing CHANGEME placeholder in existing config with real DB password."
|
|
||||||
sed -i "s|postgres://patch_manager:CHANGEME@localhost/patch_manager|postgres://${DB_USER}:${db_password}@localhost/${DB_NAME}|" "${config_file}"
|
|
||||||
else
|
|
||||||
info "Config file ${config_file} already exists with a real password, leaving it unchanged."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# No config file — create from example
|
|
||||||
if [[ -z "${db_password}" ]]; then
|
|
||||||
db_password=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 32)
|
|
||||||
psql_run -c "ALTER ROLE ${DB_USER} WITH PASSWORD '${db_password}';" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
info "Writing configuration file..."
|
|
||||||
cp /usr/share/patch-manager/config.example.toml "${config_file}"
|
|
||||||
sed -i "s|postgres://patch_manager:CHANGEME@localhost/patch_manager|postgres://${DB_USER}:${db_password}@localhost/${DB_NAME}|" "${config_file}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
chown patch-manager:patch-manager "${config_file}"
|
|
||||||
chmod 640 "${config_file}"
|
|
||||||
info "Configuration written to ${config_file}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 8. Generate JWT keys (idempotent)
|
|
||||||
# Only generates if missing; regenerates verify.pem from signing.pem if lost.
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
generate_jwt_keys() {
|
|
||||||
if [[ ! -f "${CONFIG_DIR}/jwt/signing.pem" ]]; then
|
|
||||||
info "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"
|
|
||||||
info "JWT keys generated."
|
|
||||||
elif [[ ! -f "${CONFIG_DIR}/jwt/verify.pem" ]]; then
|
|
||||||
info "Regenerating missing JWT verification key from existing signing key..."
|
|
||||||
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/verify.pem"
|
|
||||||
chmod 644 "${CONFIG_DIR}/jwt/verify.pem"
|
|
||||||
info "JWT verification key regenerated."
|
|
||||||
else
|
|
||||||
info "JWT keys already exist, skipping."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 9. Enable and start services
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
enable_and_start_services() {
|
|
||||||
systemctl daemon-reload
|
|
||||||
|
|
||||||
# Enable the target (which pulls in web + worker)
|
|
||||||
systemctl enable patch-manager.target 2>/dev/null || true
|
|
||||||
|
|
||||||
# Start or restart services
|
|
||||||
if systemctl is-active --quiet patch-manager.target 2>/dev/null; then
|
|
||||||
info "Restarting patch-manager services (upgrade)..."
|
|
||||||
systemctl restart patch-manager.target 2>/dev/null || true
|
|
||||||
else
|
|
||||||
info "Starting patch-manager services..."
|
|
||||||
systemctl start patch-manager.target 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 10. Install backup cron (idempotent)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
install_backup_cron() {
|
|
||||||
if ! crontab -l 2>/dev/null | grep -qF "backup.sh"; then
|
|
||||||
(crontab -l 2>/dev/null; echo "0 2 * * * /usr/local/bin/backup.sh >> /var/log/patch-manager/backup.log 2>&1") | crontab -
|
|
||||||
info "Nightly backup cron installed."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Main
|
|
||||||
# =============================================================================
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
configure)
|
configure)
|
||||||
create_service_user
|
# Create service user if not exists
|
||||||
create_directories
|
if ! id patch-manager &>/dev/null; then
|
||||||
wait_for_postgresql
|
useradd --system --no-create-home --shell /usr/sbin/nologin \
|
||||||
setup_database
|
--comment "Linux Patch Manager service account" patch-manager
|
||||||
apply_migrations
|
fi
|
||||||
generate_admin_password
|
|
||||||
write_config
|
|
||||||
generate_jwt_keys
|
|
||||||
enable_and_start_services
|
|
||||||
install_backup_cron
|
|
||||||
|
|
||||||
# Clean up temp file
|
# Create required directories
|
||||||
rm -f /tmp/.pm-db-password-new
|
mkdir -p /etc/patch-manager/ca /etc/patch-manager/certs \
|
||||||
|
/etc/patch-manager/jwt /etc/patch-manager/tls \
|
||||||
|
/var/log/patch-manager /opt/patch-manager \
|
||||||
|
/var/backups/patch-manager
|
||||||
|
|
||||||
info "Linux Patch Manager installation complete."
|
chown -R patch-manager:patch-manager \
|
||||||
|
/etc/patch-manager /var/log/patch-manager \
|
||||||
|
/opt/patch-manager /usr/share/patch-manager/frontend
|
||||||
|
|
||||||
|
chmod 750 /etc/patch-manager/ca /etc/patch-manager/jwt
|
||||||
|
chmod 700 /var/backups/patch-manager
|
||||||
|
|
||||||
|
# Generate JWT signing key if not present
|
||||||
|
if [[ ! -f /etc/patch-manager/jwt/signing.pem ]]; then
|
||||||
|
openssl genpkey -algorithm ed25519 -out /etc/patch-manager/jwt/signing.pem 2>/dev/null
|
||||||
|
openssl pkey -in /etc/patch-manager/jwt/signing.pem -pubout -out /etc/patch-manager/jwt/verify.pem 2>/dev/null
|
||||||
|
chown patch-manager:patch-manager /etc/patch-manager/jwt/signing.pem /etc/patch-manager/jwt/verify.pem
|
||||||
|
chmod 600 /etc/patch-manager/jwt/signing.pem
|
||||||
|
chmod 644 /etc/patch-manager/jwt/verify.pem
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Write default config if not present
|
||||||
|
if [[ ! -f /etc/patch-manager/config.toml ]]; then
|
||||||
|
cp /usr/share/patch-manager/config.example.toml /etc/patch-manager/config.toml
|
||||||
|
chown patch-manager:patch-manager /etc/patch-manager/config.toml
|
||||||
|
chmod 640 /etc/patch-manager/config.toml
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install backup cron if not present
|
||||||
|
if ! crontab -l 2>/dev/null | grep -qF "backup.sh"; then
|
||||||
|
(crontab -l 2>/dev/null; echo "0 2 * * * /usr/local/bin/backup.sh >> /var/log/patch-manager/backup.log 2>&1") | crontab -
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reload systemd
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# Restart services if this is an upgrade (not a fresh install)
|
||||||
|
if systemctl is-active --quiet patch-manager-web 2>/dev/null; then
|
||||||
|
systemctl restart patch-manager-web || true
|
||||||
|
fi
|
||||||
|
if systemctl is-active --quiet patch-manager-worker 2>/dev/null; then
|
||||||
|
systemctl restart patch-manager-worker || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run pending database migrations
|
||||||
|
MIGRATION_DIR="/usr/share/patch-manager/migrations"
|
||||||
|
if [[ -d "$MIGRATION_DIR" ]]; then
|
||||||
|
echo "Applying database migrations..."
|
||||||
|
for sql_file in $(ls "$MIGRATION_DIR"/*.sql 2>/dev/null | sort); do
|
||||||
|
echo " Applying: $(basename "$sql_file")"
|
||||||
|
done
|
||||||
|
echo "Note: Migrations must be applied manually: sudo -u patch_manager psql -d patch_manager -f <migration_file>"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Linux Patch Manager installed successfully!"
|
||||||
|
echo "==========================================="
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Install and configure PostgreSQL:"
|
||||||
|
echo " apt install postgresql-16"
|
||||||
|
echo " 2. Create the database:"
|
||||||
|
echo " sudo -u postgres createdb -O patch_manager patch_manager"
|
||||||
|
echo " 3. Edit /etc/patch-manager/config.toml with your database URL"
|
||||||
|
echo " 4. Enable and start services:"
|
||||||
|
echo " systemctl enable --now patch-manager.target"
|
||||||
|
echo " 5. Access the web UI at https://localhost"
|
||||||
|
echo " Default admin credentials are set via the seed migration."
|
||||||
|
echo ""
|
||||||
|
echo "IMPORTANT: Change the default admin password immediately after first login!"
|
||||||
|
echo ""
|
||||||
|
echo "If this is an upgrade, services have been restarted automatically."
|
||||||
|
echo "Apply any new database migrations:"
|
||||||
|
echo " sudo -u patch_manager psql -d patch_manager -f /usr/share/patch-manager/migrations/<NNN_migration>.sql"
|
||||||
|
echo ""
|
||||||
;;
|
;;
|
||||||
|
|
||||||
abort-upgrade|abort-remove|abort-deconfigure)
|
abort-upgrade|abort-remove|abort-deconfigure)
|
||||||
|
|||||||
@ -1,58 +0,0 @@
|
|||||||
# =============================================================================
|
|
||||||
# Linux Patch Manager — Docker Compose Deployment
|
|
||||||
# =============================================================================
|
|
||||||
# Usage:
|
|
||||||
# cp .env.example .env # Edit DB_PASSWORD
|
|
||||||
# docker compose up -d
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
services:
|
|
||||||
db:
|
|
||||||
image: postgres:16
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: patch_manager
|
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
|
||||||
POSTGRES_DB: patch_manager
|
|
||||||
volumes:
|
|
||||||
- pgdata:/var/lib/postgresql/data
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U patch_manager -d patch_manager"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 10
|
|
||||||
start_period: 10s
|
|
||||||
networks:
|
|
||||||
- patch-manager-net
|
|
||||||
|
|
||||||
app:
|
|
||||||
image: ghcr.io/draco-lunaris/linux-patch-manager:${TAG:-latest}
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
ports:
|
|
||||||
- "443:443"
|
|
||||||
environment:
|
|
||||||
DATABASE_URL: postgres://patch_manager:${DB_PASSWORD}@db:5432/patch_manager
|
|
||||||
PATCH_MANAGER_CONFIG: /etc/patch-manager/config.toml
|
|
||||||
volumes:
|
|
||||||
- pm-config:/etc/patch-manager
|
|
||||||
- pm-logs:/var/log/patch-manager
|
|
||||||
- pm-data:/opt/patch-manager
|
|
||||||
networks:
|
|
||||||
- patch-manager-net
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
pgdata:
|
|
||||||
driver: local
|
|
||||||
pm-config:
|
|
||||||
driver: local
|
|
||||||
pm-logs:
|
|
||||||
driver: local
|
|
||||||
pm-data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
networks:
|
|
||||||
patch-manager-net:
|
|
||||||
driver: bridge
|
|
||||||
@ -1,232 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "patch-manager-ui",
|
"name": "patch-manager-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.8",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@ -22,7 +22,7 @@ warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
|||||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
|
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
|
||||||
|
|
||||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
VERSION="1.1.8"
|
VERSION="1.0.0"
|
||||||
RELEASE="1"
|
RELEASE="1"
|
||||||
PKG_NAME="linux-patch-manager"
|
PKG_NAME="linux-patch-manager"
|
||||||
DEB_NAME="${PKG_NAME}_${VERSION}-${RELEASE}_amd64.deb"
|
DEB_NAME="${PKG_NAME}_${VERSION}-${RELEASE}_amd64.deb"
|
||||||
|
|||||||
Reference in New Issue
Block a user