From 22a35e683fad201ff9696ca0b8d600fd14c04e1f Mon Sep 17 00:00:00 2001 From: DASimp Date: Fri, 8 May 2026 12:21:37 -0500 Subject: [PATCH 1/5] Add Docker-based test suite and fix install/uninstall for all 6 OSes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Test infrastructure (new) - tests/run-tests.sh: orchestrates build → install → verify → uninstall → verify across all 6 supported OSes; runs each OS sequentially with full logging to tests/logs// - tests/verify-install.sh: 24-point post-install checklist (services running, files present, DB accessible, SimpleRisk web UI reachable, cron job installed, SSL cert present, PHP settings applied) - tests/verify-uninstall.sh: 12-point post-uninstall checklist (packages removed, files/dirs gone, DB dropped, cron job removed) - tests/dockerfiles/Dockerfile.ubuntu-{22.04,24.04}: pre-install lamp-server^ with policy-rc.d deny so services are installed but not started; start them before running the setup script to match a real server state - tests/dockerfiles/Dockerfile.debian-{12,13}: /etc/init.d/mysql shim that manages mysqld directly (no systemd); /usr/sbin/policy-rc.d deny gate during image build - tests/dockerfiles/Dockerfile.centos-stream-{9,10}: /usr/local/bin/systemctl shim (takes PATH precedence over real systemctl installed by MySQL's systemd RPM dep) that manages mysqld and httpd directly; no-op shims for firewall-cmd, setsebool, chcon; innodb_use_native_aio=0 for Docker overlayfs compatibility - .github/workflows/install-test.yml: CI matrix over all 6 OSes on push/PR to main - CLAUDE.md: project documentation for Claude Code ## Bug fixes in simplerisk-setup.sh - Add --uninstall flag: routes to per-OS uninstall functions with dedicated ask_user_uninstall() prompt and uninstall_final_message(); previously the flag was accepted but silently fell through to installation - CentOS/RHEL: add php-cli to explicit dnf install list — on el10 it is a Recommended (weak) dep of php and was being silently omitted - CentOS/RHEL: add --exclude 'mysql8.4*' to mysql-community-server install — CentOS Stream 10's AppStream ships mysql8.4-server which conflicts on /usr/sbin/mysqld; the exclude is a no-op on el9/el8 All 6 OS tests (ubuntu-22.04, ubuntu-24.04, debian-12, debian-13, centos-stream-9, centos-stream-10) pass the full 7-step suite locally on Docker Desktop for Windows. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/install-test.yml | 138 ++++++++++ CLAUDE.md | 95 +++++++ README.md | 10 +- simplerisk-setup.sh | 228 ++++++++++++++-- tests/dockerfiles/Dockerfile.centos-stream-10 | 156 +++++++++++ tests/dockerfiles/Dockerfile.centos-stream-9 | 156 +++++++++++ tests/dockerfiles/Dockerfile.debian-12 | 100 +++++++ tests/dockerfiles/Dockerfile.debian-13 | 100 +++++++ tests/dockerfiles/Dockerfile.ubuntu-22.04 | 44 ++++ tests/dockerfiles/Dockerfile.ubuntu-24.04 | 35 +++ tests/run-tests.sh | 243 ++++++++++++++++++ tests/verify-install.sh | 125 +++++++++ tests/verify-uninstall.sh | 118 +++++++++ 13 files changed, 1527 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/install-test.yml create mode 100644 CLAUDE.md create mode 100644 tests/dockerfiles/Dockerfile.centos-stream-10 create mode 100644 tests/dockerfiles/Dockerfile.centos-stream-9 create mode 100644 tests/dockerfiles/Dockerfile.debian-12 create mode 100644 tests/dockerfiles/Dockerfile.debian-13 create mode 100644 tests/dockerfiles/Dockerfile.ubuntu-22.04 create mode 100644 tests/dockerfiles/Dockerfile.ubuntu-24.04 create mode 100644 tests/run-tests.sh create mode 100644 tests/verify-install.sh create mode 100644 tests/verify-uninstall.sh diff --git a/.github/workflows/install-test.yml b/.github/workflows/install-test.yml new file mode 100644 index 0000000..8182ec3 --- /dev/null +++ b/.github/workflows/install-test.yml @@ -0,0 +1,138 @@ +name: SimpleRisk Install/Uninstall Tests + +on: + push: + branches: [main, "feature/**"] + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + test: + name: "Test: ${{ matrix.os-slug }}" + runs-on: ubuntu-latest + + strategy: + # Run all matrix jobs even if one fails so we get a full picture + fail-fast: false + matrix: + os-slug: + - ubuntu-22.04 + - ubuntu-24.04 + - debian-12 + - debian-13 + - centos-stream-9 + - centos-stream-10 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # ── Build ─────────────────────────────────────────────────────────────── + - name: Build test image + run: | + docker build \ + -f tests/dockerfiles/Dockerfile.${{ matrix.os-slug }} \ + -t simplerisk-test:${{ matrix.os-slug }} \ + tests/dockerfiles/ + + # ── Start — Debian/Ubuntu (no systemd needed) ─────────────────────────── + - name: Start container (Debian/Ubuntu) + if: ${{ !startsWith(matrix.os-slug, 'centos') }} + run: | + docker run -d \ + --name simplerisk-test-${{ matrix.os-slug }} \ + --privileged \ + simplerisk-test:${{ matrix.os-slug }} \ + /bin/bash -c "tail -f /dev/null" + + # ── Start — CentOS/RHEL (systemd as PID 1) ───────────────────────────── + - name: Start container (CentOS/RHEL — systemd) + if: ${{ startsWith(matrix.os-slug, 'centos') }} + run: | + docker run -d \ + --name simplerisk-test-${{ matrix.os-slug }} \ + --privileged \ + --cgroupns=host \ + --tmpfs /run \ + --tmpfs /run/lock \ + simplerisk-test:${{ matrix.os-slug }} + + - name: Wait for systemd (CentOS/RHEL only) + if: ${{ startsWith(matrix.os-slug, 'centos') }} + run: | + CONTAINER="simplerisk-test-${{ matrix.os-slug }}" + echo "Waiting for systemd multi-user.target..." + for i in $(seq 1 30); do + if docker exec "$CONTAINER" systemctl is-active --quiet multi-user.target 2>/dev/null; then + echo "systemd ready." + break + fi + sleep 3 + if [ "$i" -eq 30 ]; then + echo "ERROR: systemd did not reach multi-user.target within 90s" + docker exec "$CONTAINER" journalctl -n 100 2>/dev/null || true + exit 1 + fi + done + + # ── Copy files ────────────────────────────────────────────────────────── + - name: Copy scripts into container + run: | + CONTAINER="simplerisk-test-${{ matrix.os-slug }}" + docker cp simplerisk-setup.sh "$CONTAINER:/root/simplerisk-setup.sh" + docker cp tests/verify-install.sh "$CONTAINER:/root/verify-install.sh" + docker cp tests/verify-uninstall.sh "$CONTAINER:/root/verify-uninstall.sh" + docker exec "$CONTAINER" chmod +x \ + /root/simplerisk-setup.sh \ + /root/verify-install.sh \ + /root/verify-uninstall.sh + + # ── Install ───────────────────────────────────────────────────────────── + - name: Run install + run: | + docker exec simplerisk-test-${{ matrix.os-slug }} \ + bash /root/simplerisk-setup.sh --yes --debug + + # ── Verify install ────────────────────────────────────────────────────── + - name: Verify installation + run: | + docker exec simplerisk-test-${{ matrix.os-slug }} \ + bash /root/verify-install.sh + + # ── Uninstall ─────────────────────────────────────────────────────────── + - name: Run uninstall + run: | + docker exec simplerisk-test-${{ matrix.os-slug }} \ + bash /root/simplerisk-setup.sh --uninstall --yes --debug + + # ── Verify uninstall ──────────────────────────────────────────────────── + - name: Verify uninstallation + run: | + docker exec simplerisk-test-${{ matrix.os-slug }} \ + bash /root/verify-uninstall.sh + + # ── Diagnostics on failure ────────────────────────────────────────────── + - name: Collect diagnostics on failure + if: failure() + run: | + CONTAINER="simplerisk-test-${{ matrix.os-slug }}" + echo "=== journalctl (last 100 lines) ===" && \ + docker exec "$CONTAINER" journalctl -n 100 2>/dev/null || true + echo "=== Apache error log ===" && \ + docker exec "$CONTAINER" cat /var/log/apache2/error.log 2>/dev/null || \ + docker exec "$CONTAINER" cat /var/log/httpd/error_log 2>/dev/null || true + echo "=== MySQL error log ===" && \ + docker exec "$CONTAINER" cat /var/log/mysql/error.log 2>/dev/null || \ + docker exec "$CONTAINER" cat /var/log/mysqld.log 2>/dev/null || true + echo "=== /root/passwords.txt ===" && \ + docker exec "$CONTAINER" cat /root/passwords.txt 2>/dev/null || true + + # ── Teardown ──────────────────────────────────────────────────────────── + - name: Teardown container + if: always() + run: | + docker rm -f simplerisk-test-${{ matrix.os-slug }} 2>/dev/null || true diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..52afdea --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,95 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this repository is + +A single Bash script (`simplerisk-setup.sh`) that installs or uninstalls [SimpleRisk](https://www.simplerisk.com/) (an open-source risk management application) on supported Linux servers. It sets up a full LAMP stack (Apache, MySQL 8.4 LTS, PHP 8.5) with SSL, firewall rules, sendmail/postfix, and a backup cron job. + +## Supported operating systems + +| OS | Versions | +|---|---| +| Ubuntu | 22.04 LTS, 24.x, 25.x | +| Debian | 12, 13 | +| CentOS Stream | 9, 10 | +| RHEL / RHEL Server | 9.x, 10.x | +| SLES | 15.7+ (requires PHP module activated via `suseconnect`) | + +## Automated testing + +Docker-based tests live in `tests/`. They cover install and uninstall for every supported OS except RHEL (requires paid subscription) and SLES (requires subscription + `suseconnect`). + +```bash +# Run all OSes +bash tests/run-tests.sh + +# Run a single OS +bash tests/run-tests.sh ubuntu-22.04 +bash tests/run-tests.sh debian-12 +bash tests/run-tests.sh centos-stream-9 +``` + +Available slugs: `ubuntu-22.04`, `ubuntu-24.04`, `debian-12`, `debian-13`, `centos-stream-9`, `centos-stream-10`. + +Each test run: builds the image → starts the container → installs SimpleRisk (`--yes --debug`) → runs `tests/verify-install.sh` inside the container → uninstalls (`--uninstall --yes --debug`) → runs `tests/verify-uninstall.sh`. Logs land in `tests/logs//`. + +**CentOS/RHEL containers require `--privileged` and systemd as PID 1.** The Dockerfiles handle this. Debian/Ubuntu containers use `--privileged` only for UFW; no systemd is needed because the script calls `service` on those distros. + +CI runs automatically via `.github/workflows/install-test.yml` on push/PR to `main` using a matrix across all six OSes (`fail-fast: false`). + +## Validating the script + +The only "test" available is the OS validation dry-run — no build system or test suite exists: + +```bash +sudo bash simplerisk-setup.sh --validate-os-only +``` + +This validates that the detected OS/version is supported without requiring root and without making any changes. + +## Running the script + +```bash +# Interactive install +sudo bash simplerisk-setup.sh + +# Headless install (auto-yes) +sudo bash simplerisk-setup.sh --yes + +# Debug mode (shows all command output) +sudo bash simplerisk-setup.sh --debug + +# Install the current testing release +sudo bash simplerisk-setup.sh --testing + +# Uninstall SimpleRisk and all associated components +sudo bash simplerisk-setup.sh --uninstall +``` + +## Architecture + +The script is structured as a single file with clearly delimited sections: + +- **Main flow** (`setup`, `validate_args`, `check_root`, `ask_user`, `load_os_variables`, `validate_os_and_version`) — orchestrates the overall run. `setup()` is the entry point called at the bottom of the file. +- **Installation dispatcher** (`perform_installation`) — calls the appropriate OS-specific setup function. +- **Uninstallation dispatcher** (`perform_uninstallation`) — calls the appropriate OS-specific uninstall function. +- **Auxiliary/shared functions** — `set_up_database`, `set_up_simplerisk`, `set_php_settings`, `set_up_backup_cronjob`, `generate_passwords`, `drop_simplerisk_database`, `remove_backup_cronjob`, etc. These are called by the OS-specific functions. +- **OS setup functions** — `setup_ubuntu_debian`, `setup_centos_rhel`, `setup_suse`. Each handles its package manager, repos, service management, and config file paths. +- **OS uninstall functions** — `uninstall_ubuntu_debian`, `uninstall_centos_rhel`, `uninstall_suse`. Mirror the setup functions in reverse. + +## Keeping README.md in sync + +After any change to `simplerisk-setup.sh`, check whether `README.md` needs updating. The areas most likely to drift: + +- **Supported OS/version table** — if a new distro or version is added/removed, update the list in the README. +- **`--help` flags block** — the README contains a verbatim copy of the help output. If flags are added, removed, or their descriptions change, mirror those changes in the README's ` ```--help``` ` section and its synopsis line. +- **SLES minimum SP** — the `SLES_15_SUPPORTED_SP` constant drives the version check; the README must reflect the same value. + +## Key conventions + +- `exec_cmd` wraps a command and calls `bail` on failure. Use this for steps that must succeed. +- `exec_cmd_nobail` runs a command but does not abort on failure. Use this for cleanup/uninstall steps where partial state is acceptable. +- OS detection populates `$OS` and `$VER`; `validate_os_and_version` sets `$SETUP_TYPE` to `debian`, `rhel`, or `suse`. All OS-specific branching elsewhere uses `$SETUP_TYPE` or `$OS`/`$VER` directly. +- MySQL root password is stored in `/root/passwords.txt` (mode 600) after install. The uninstall path reads it back via `get_mysql_root_password` to drop the database. +- SLES requires the PHP8 module to be activated in the subscription before running the script. diff --git a/README.md b/README.md index ccd89b2..1dc1771 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ - Debian 12, 13 - CentOS Stream 9, 10 - Red Hat Enterprise Linux (RHEL) 9, 10 -- SUSE Linux Enterprise Server (SLES) higher than 15.5 +- SUSE Linux Enterprise Server (SLES) 15.7 or higher ## Instructions @@ -20,14 +20,16 @@ Run as root or insert `sudo -E` before `bash`: ## `--help` ``` -Script to set up SimpleRisk on a server. +Script to set up or uninstall SimpleRisk on a server. -./simplerisk-setup [-d|--debug] [-n|--no-assistance] [-h|--help] [--validate-os-only] +./simplerisk-setup [-d|--debug] [--yes] [-h|--help] [--validate-os-only] [--uninstall] Flags: -d|--debug: Shows the output of the commands being run by this script --n|--no-assistance: Runs the script in headless mode (will assume yes on anything) -t|--testing: Picks the current testing version --validate-os-only: Only validates if the current host (OS and version) are supported by the script. This option does not require running the script as superuser. +--uninstall: Removes SimpleRisk and all associated packages, services, and data + WARNING: This action is irreversible and will destroy all SimpleRisk data. +--yes: Will answer yes on every question (Use it carefully) -h|--help: Shows instructions on how to use this script ``` diff --git a/simplerisk-setup.sh b/simplerisk-setup.sh index b4aa6f1..fde4428 100755 --- a/simplerisk-setup.sh +++ b/simplerisk-setup.sh @@ -25,14 +25,22 @@ setup (){ fi # Ask user input unless it is on headless mode or validating if the script works if [ ! -v HEADLESS ] && [ ! -v VALIDATE_ONLY ]; then - ask_user + if [ -v UNINSTALL ]; then + ask_user_uninstall + else + ask_user + fi fi load_os_variables validate_os_and_version if [ -v VALIDATE_ONLY ]; then exit 0 fi - perform_installation + if [ -v UNINSTALL ]; then + perform_uninstallation + else + perform_installation + fi } validate_args(){ @@ -52,6 +60,9 @@ validate_args(){ --validate-os-only) VALIDATE_ONLY=y shift;; + --uninstall) + UNINSTALL=y + shift;; -h|--help) print_help exit 0;; @@ -71,7 +82,7 @@ check_root() { } ask_user(){ - if [ ! -t 0 ] && [ ! -v HEADLESS ]; then + if [ ! -t 0 ] && [ ! -v HEADLESS ]; then print_error_message "No interactive terminal available. Re-run with --yes." fi @@ -82,6 +93,18 @@ ask_user(){ esac } +ask_user_uninstall(){ + if [ ! -t 0 ] && [ ! -v HEADLESS ]; then + print_error_message "No interactive terminal available. Re-run with --yes." + fi + + read -r -p 'This script will UNINSTALL SimpleRisk and remove all associated packages, files, and data. This action is IRREVERSIBLE. Proceed? [ Yes / (N)o ]: ' answer < /dev/tty + case "${answer}" in + Yes|yes|Y|y ) ;; + * ) exit 1;; + esac +} + load_os_variables(){ # freedesktop.org and systemd if [ -f /etc/os-release ]; then @@ -186,6 +209,17 @@ perform_installation() { success_final_message } +perform_uninstallation() { + case "${SETUP_TYPE:-}" in + debian) uninstall_ubuntu_debian;; + rhel) uninstall_centos_rhel;; + suse) uninstall_suse;; + *) print_error_message "Could not validate the setup type. Check the perform_uninstallation and validate_os_and_version functions.";; + esac + + uninstall_final_message +} + ######################### ## AUXILIARY FUNCTIONS ## ######################### @@ -307,12 +341,18 @@ success_final_message(){ print_status 'INSTALLATION COMPLETED SUCCESSFULLY' } +uninstall_final_message(){ + print_status 'UNINSTALLATION COMPLETED SUCCESSFULLY' + print_status 'SimpleRisk and its associated components have been removed from this system.' + print_status 'Note: Base packages (wget, gnupg, cron, etc.) that were present before installation may remain on the system.' +} + print_help() { cat << EOC -Script to set up SimpleRisk on a server. +Script to set up or uninstall SimpleRisk on a server. -./simplerisk-setup [-d|--debug] [--yes] [-h|--help] [--validate-os-only] +./simplerisk-setup [-d|--debug] [--yes] [-h|--help] [--validate-os-only] [--uninstall] Flags: -d|--debug: Shows the output of the commands being run by this script @@ -320,6 +360,9 @@ Flags: --validate-os-only: Only validates if the current host (OS and version) are supported by the script. This option does not require running the script as superuser. +--uninstall: Removes SimpleRisk and all associated packages, services, and data + (Apache/httpd, MySQL, PHP, sendmail/postfix, firewall rules). + WARNING: This action is irreversible and will destroy all SimpleRisk data. --yes: Will answer yes on every question (Use it carefully) -h|--help: Shows instructions on how to use this script EOC @@ -329,6 +372,29 @@ bail() { print_error_message 'The command exited with failure. Verify the command output or run the script in debug mode (-d|--debug).' } +remove_backup_cronjob() { + (crontab -l 2>/dev/null | grep -v 'simplerisk/cron/cron.php') | crontab - 2>/dev/null || true +} + +get_mysql_root_password() { + if [ -f /root/passwords.txt ]; then + grep 'MYSQL ROOT PASSWORD:' /root/passwords.txt | awk -F ': ' '{print $2}' + fi +} + +drop_simplerisk_database() { + local root_password + root_password=$(get_mysql_root_password) + if [ -n "${root_password}" ]; then + exec_cmd_nobail "mysql -uroot --password='${root_password}' -e \"DROP DATABASE IF EXISTS simplerisk\"" + exec_cmd_nobail "mysql -uroot --password='${root_password}' -e \"DROP USER IF EXISTS 'simplerisk'@'localhost'\"" + exec_cmd_nobail "mysql -uroot --password='${root_password}' -e \"FLUSH PRIVILEGES\"" + else + print_status 'WARNING: Could not find MySQL root password in /root/passwords.txt. Skipping database removal.' + print_status 'You may need to manually drop the simplerisk database and user.' + fi +} + ######################## ## OS SETUP FUNCTIONS ## ######################## @@ -354,9 +420,8 @@ setup_ubuntu_debian(){ exec_cmd "echo 'deb [signed-by=/etc/apt/keyrings/sury-php.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main' | sudo tee /etc/apt/sources.list.d/sury-php.list" print_status 'Adding MySQL 8 repository' - exec_cmd "export GNUPGHOME=\"$(mktemp -d)\"" - exec_cmd "gpg --batch --keyserver keys.gnupg.net --recv-keys $MYSQL_GPG_KEY || gpg --batch --keyserver pgp.mit.edu --recv-keys $MYSQL_GPG_KEY" # Fallback server taken from https://dev.mysql.com/doc/refman/8.4/en/checking-gpg-signature.html - exec_cmd "gpg --batch --export $MYSQL_GPG_KEY | sudo tee /etc/apt/trusted.gpg.d/mysql.gpg >> /dev/null" + # Download the signing key directly from MySQL (more reliable than keyservers). + exec_cmd "curl -fsSL '$MYSQL_KEY_URL' | gpg --dearmor -o /etc/apt/trusted.gpg.d/mysql.gpg" exec_cmd "echo 'deb [signed-by=/etc/apt/trusted.gpg.d/mysql.gpg] https://repo.mysql.com/apt/$(lsb_release -si | tr '[:upper:]' '[:lower:]')/ $(lsb_release -sc) mysql-8.4-lts' | sudo tee /etc/apt/sources.list.d/mysql.list" print_status 'Re-populating apt-get cache with added repos...' @@ -369,6 +434,8 @@ setup_ubuntu_debian(){ if [ "${OS}" = "${UBUNTU_OSVAR}" ]; then print_status 'Installing lamp-server...' exec_cmd 'apt-get install -y lamp-server^' + print_status 'Installing cron...' + exec_cmd 'apt-get install -y cron' else print_status 'Installing Apache...' exec_cmd 'apt-get install -y apache2' @@ -430,15 +497,20 @@ setup_ubuntu_debian(){ fi print_status 'Configuring Sendmail...' - exec_cmd "sed -i 's/\(localhost\)/\1 $(hostname)/g' /etc/hosts" + # /etc/hosts may be a bind-mount (e.g. Docker) and cannot be renamed by + # `sed -i`. Write to a temp file then overwrite the original in place. + exec_cmd "sed 's/\(localhost\)/\1 $(hostname)/g' /etc/hosts > /tmp/hosts.bak && cat /tmp/hosts.bak > /etc/hosts && rm -f /tmp/hosts.bak" exec_cmd 'yes | sendmailconfig' - exec_cmd 'service sendmail start' + exec_cmd 'service sendmail restart' print_status 'Restarting Apache to load the new configuration...' exec_cmd 'service apache2 restart' generate_passwords + print_status 'Ensuring MySQL database server is running...' + exec_cmd 'service mysql status > /dev/null 2>&1 || service mysql start' + print_status 'Configuring MySQL...' exec_cmd "sed -i '$ a sql-mode=\"NO_ENGINE_SUBSTITUTION\"' /etc/mysql/mysql.conf.d/mysqld.cnf" set_up_database @@ -452,10 +524,8 @@ setup_ubuntu_debian(){ print_status 'Setting up Backup cronjob...' set_up_backup_cronjob - if [ "${OS}" = "${DEBIAN_OSVAR}" ]; then - print_status 'Installing UFW firewall...' - exec_cmd 'apt-get install -y ufw' - fi + print_status 'Installing UFW firewall...' + exec_cmd 'apt-get install -y ufw' print_status 'Enabling UFW firewall...' exec_cmd 'ufw allow ssh' @@ -495,12 +565,16 @@ setup_centos_rhel(){ print_status 'Installing PHP for Apache...' exec_cmd "dnf -y module reset php" exec_cmd "dnf -y module enable php:remi-8.5" - exec_cmd "dnf -y install httpd php php-common php-mysqlnd php-mbstring php-opcache php-gd php-zip php-json php-ldap php-curl php-xml php-intl php-process" + exec_cmd "dnf -y install httpd php php-cli php-common php-mysqlnd php-mbstring php-opcache php-gd php-zip php-json php-ldap php-curl php-xml php-intl php-process" set_php_settings /etc/php.ini print_status 'Installing the MySQL database server...' - exec_cmd "dnf install -y mysql-community-server --exclude mariadb*" + # On CentOS/RHEL 10, AppStream ships mysql8.4-server which conflicts with + # mysql-community-server (same files). Exclude it explicitly so DNF does + # not pull it in as a weak dependency. The exclude is a no-op on el9/el8 + # where mysql8.4-server does not exist in any enabled repo. + exec_cmd "dnf install -y mysql-community-server --exclude 'mariadb*' --exclude 'mysql8.4*'" print_status 'Enabling and starting MySQL database server...' exec_cmd 'systemctl enable mysqld' @@ -571,7 +645,7 @@ EOF exec_cmd 'systemctl start httpd' print_status 'Configuring and starting Sendmail...' - exec_cmd "sed -i 's/\(localhost\)/\1 $(hostname)/g' /etc/hosts" + exec_cmd "sed 's/\(localhost\)/\1 $(hostname)/g' /etc/hosts > /tmp/hosts.bak && cat /tmp/hosts.bak > /etc/hosts && rm -f /tmp/hosts.bak" exec_cmd 'systemctl start sendmail' print_status 'Opening Firewall for HTTP/HTTPS traffic' @@ -739,5 +813,125 @@ EOF fi } +########################### +## OS UNINSTALL FUNCTIONS ## +########################### +uninstall_ubuntu_debian(){ + export DEBIAN_FRONTEND=noninteractive + + print_status 'Removing SimpleRisk cron job...' + remove_backup_cronjob + + print_status 'Dropping SimpleRisk database and user...' + drop_simplerisk_database + + print_status 'Stopping services...' + exec_cmd_nobail 'service apache2 stop' + exec_cmd_nobail 'service mysql stop' + exec_cmd_nobail 'service sendmail stop' + + print_status 'Removing SimpleRisk application files...' + exec_cmd_nobail 'rm -rf /var/www/simplerisk' + + print_status 'Removing installed packages...' + exec_cmd "apt-get purge -y 'php*' 'libapache2-mod-php*' apache2 apache2-utils apache2-bin mysql-server mysql-client mysql-common sendmail sendmail-bin" + exec_cmd 'apt-get autoremove -y' + exec_cmd 'apt-get autoclean' + + if [ "${OS}" = "${DEBIAN_OSVAR}" ]; then + print_status 'Removing added repositories and keys...' + exec_cmd_nobail 'rm -f /etc/apt/sources.list.d/sury-php.list' + exec_cmd_nobail 'rm -f /etc/apt/sources.list.d/mysql.list' + exec_cmd_nobail 'rm -f /etc/apt/keyrings/sury-php.gpg' + exec_cmd_nobail 'rm -f /etc/apt/trusted.gpg.d/mysql.gpg' + exec_cmd_nobail 'apt-get update' + fi + + print_status 'Removing UFW firewall rules for SimpleRisk...' + exec_cmd_nobail 'ufw delete allow http' + exec_cmd_nobail 'ufw delete allow https' + + print_status 'Removing MySQL password file...' + exec_cmd_nobail 'rm -f /root/passwords.txt' +} + +uninstall_centos_rhel(){ + print_status 'Removing SimpleRisk cron job...' + remove_backup_cronjob + + print_status 'Dropping SimpleRisk database and user...' + drop_simplerisk_database + + print_status 'Stopping and disabling services...' + exec_cmd_nobail 'systemctl stop httpd' + exec_cmd_nobail 'systemctl disable httpd' + exec_cmd_nobail 'systemctl stop mysqld' + exec_cmd_nobail 'systemctl disable mysqld' + exec_cmd_nobail 'systemctl stop sendmail' + + print_status 'Removing SimpleRisk application files...' + exec_cmd_nobail 'rm -rf /var/www/simplerisk' + + print_status 'Removing SimpleRisk Apache virtual host config...' + exec_cmd_nobail 'rm -f /etc/httpd/sites-enabled/simplerisk.conf' + exec_cmd_nobail 'rm -rf /etc/httpd/sites-available /etc/httpd/sites-enabled' + exec_cmd_nobail "sed -i '/IncludeOptional sites-enabled\/\*.conf/d' /etc/httpd/conf/httpd.conf" + exec_cmd_nobail 'mv /etc/httpd/conf.d/welcome.conf.disabled /etc/httpd/conf.d/welcome.conf 2>/dev/null || true' + + print_status 'Removing installed packages...' + exec_cmd "dnf -y remove httpd mod_ssl 'php*' mysql-community-server mysql-community-client sendmail sendmail-cf m4" + exec_cmd 'dnf -y autoremove' + + print_status 'Removing MySQL and PHP repositories...' + local major_version="${VER%%.*}" + exec_cmd_nobail "rpm -e mysql84-community-release-el${major_version} 2>/dev/null || true" + exec_cmd_nobail "dnf -y remove epel-release" + exec_cmd_nobail "rpm -e remi-release-${major_version} 2>/dev/null || true" + exec_cmd_nobail "dnf clean all" + + print_status 'Removing firewall rules for SimpleRisk...' + exec_cmd_nobail 'firewall-cmd --permanent --zone=public --remove-service=http' + exec_cmd_nobail 'firewall-cmd --permanent --zone=public --remove-service=https' + exec_cmd_nobail 'firewall-cmd --reload' + + print_status 'Removing MySQL password file...' + exec_cmd_nobail 'rm -f /root/passwords.txt' +} + +uninstall_suse(){ + print_status 'Removing SimpleRisk cron job...' + remove_backup_cronjob + + print_status 'Dropping SimpleRisk database and user...' + drop_simplerisk_database + + print_status 'Stopping and disabling services...' + exec_cmd_nobail 'systemctl stop apache2' + exec_cmd_nobail 'systemctl disable apache2' + exec_cmd_nobail 'systemctl stop mysql' + exec_cmd_nobail 'systemctl disable mysql' + + print_status 'Removing SimpleRisk application files...' + exec_cmd_nobail 'rm -rf /var/www/simplerisk' + + print_status 'Removing SimpleRisk Apache virtual host and SSL config...' + exec_cmd_nobail 'rm -f /etc/apache2/vhosts.d/simplerisk.conf' + exec_cmd_nobail 'rm -f /etc/apache2/vhosts.d/ssl.conf' + exec_cmd_nobail 'rm -f /etc/apache2/ssl.key/simplerisk.key' + exec_cmd_nobail 'rm -f /etc/apache2/ssl.csr/simplerisk.csr' + exec_cmd_nobail 'rm -f /etc/apache2/ssl.crt/simplerisk.crt' + exec_cmd_nobail "sed -i '/LoadModule rewrite_module.*mod_rewrite.so/d' /etc/apache2/loadmodule.conf" + + print_status 'Removing installed packages...' + exec_cmd "zypper -n remove apache2 mysql-community-server 'php8*' apache2-mod_php8" + exec_cmd 'zypper -n autoremove' + + print_status 'Removing MySQL repository...' + exec_cmd_nobail 'rpm -e mysql84-community-release-sl15 2>/dev/null || true' + + print_status 'Removing MySQL password file...' + exec_cmd_nobail 'rm -f /root/passwords.txt' +} + ## Defer setup until we have the complete script setup "${@:1}" diff --git a/tests/dockerfiles/Dockerfile.centos-stream-10 b/tests/dockerfiles/Dockerfile.centos-stream-10 new file mode 100644 index 0000000..84d226c --- /dev/null +++ b/tests/dockerfiles/Dockerfile.centos-stream-10 @@ -0,0 +1,156 @@ +FROM quay.io/centos/centos:stream10 + +ENV container=docker + +# --allowerasing lets DNF swap curl-minimal (pre-installed) for full curl. +# cronie (crontab) and which are pre-installed on real RHEL/CentOS servers but +# absent from the minimal container image; add them so the setup script can +# install the backup cron job. +RUN dnf -y install --allowerasing curl wget sudo cronie which && \ + dnf clean all + +# MySQL uses native AIO by default, which fails on Docker's overlayfs driver. +RUN mkdir -p /etc/my.cnf.d && \ + printf '[mysqld]\ninnodb_use_native_aio=0\n' > /etc/my.cnf.d/docker.cnf + +# systemd cannot start in Docker Desktop for Windows (cgroup v2 unavailable). +# This shim replaces /usr/bin/systemctl so the setup script's +# `systemctl start/stop/restart/enable/disable/daemon-reload` calls work +# by managing processes directly instead. +RUN cat > /usr/local/bin/systemctl << 'EOF' +#!/bin/bash +# Minimal systemctl shim — handles the subset used by simplerisk-setup.sh +# on CentOS/RHEL without requiring a running systemd PID 1. + +# Strip --quiet / --system / other flags; find the action and unit name. +args=() +for arg in "$@"; do + [[ "$arg" == --* ]] && continue + args+=("$arg") +done +action="${args[0]:-}" +unit="${args[1]%.service}" # strip optional .service suffix + +start_mysqld() { + mysqladmin ping --silent >/dev/null 2>&1 && return 0 # already running + mkdir -p /var/run/mysqld && chown mysql:mysql /var/run/mysqld 2>/dev/null || true + # Pre-create the error log with mysql ownership so mysqld (running as mysql user) + # can open it for writing internally in addition to the shell redirect. + touch /var/log/mysqld.log && chown mysql:mysql /var/log/mysqld.log 2>/dev/null || true + # The RPM %post scriptlet may leave /var/lib/mysql in a partial state when + # systemd is unavailable (auto.cnf + binlog.index but no ibdata1). + # Re-initialize if ibdata1 is missing. + if [ ! -f /var/lib/mysql/ibdata1 ]; then + rm -f /var/lib/mysql/auto.cnf /var/lib/mysql/binlog.index 2>/dev/null || true + # Use --initialize (not --insecure) so a temporary root password is written + # to /var/log/mysqld.log as a "Note" line for simplerisk-setup.sh to read. + mysqld --initialize --user=mysql >>/var/log/mysqld.log 2>&1 + fi + # mysqld_safe and --daemonize were removed in MySQL 8.4; run as background process. + # Use --init-file to install the validate_password component at startup: + # the community RPM does not pre-install it when systemd is absent, but + # simplerisk-setup.sh calls SET GLOBAL validate_password.policy=LOW. + # Errors in --init-file are non-fatal, so this is safe on subsequent restarts + # when the component is already installed. + printf "INSTALL COMPONENT 'file://component_validate_password';\n" > /var/lib/mysql/docker-init.sql + # Append (>>) so the initialization "Note: temp password" line is preserved. + nohup mysqld --user=mysql --init-file=/var/lib/mysql/docker-init.sql >>/var/log/mysqld.log 2>&1 & + local i=0 + while [ $i -lt 60 ]; do + mysqladmin ping --silent >/dev/null 2>&1 && return 0 + sleep 1; i=$((i+1)) + done + echo "systemctl shim: mysqld did not start within 60s" >&2; return 1 +} + +stop_mysqld() { + # Send SIGTERM directly to the mysqld process so that no root password is + # needed (mysqladmin shutdown requires auth after setup changes the password). + local pidfile=/var/run/mysqld/mysqld.pid + if [ -f "$pidfile" ]; then + local pid + pid=$(cat "$pidfile" 2>/dev/null) + [ -n "$pid" ] && kill -TERM "$pid" 2>/dev/null || true + else + pkill -TERM mysqld 2>/dev/null || true + fi + # Wait for mysqld to fully stop before returning so that the subsequent + # start_mysqld call does not find MySQL still running and skip the restart. + local i=0 + while mysqladmin ping --silent >/dev/null 2>&1 && [ $i -lt 30 ]; do + sleep 1; i=$((i+1)) + done +} + +start_httpd() { + pgrep httpd >/dev/null 2>&1 && return 0 + # The mod_ssl %post scriptlet may skip cert generation without systemd. + # Generate a self-signed cert if missing so httpd can start. + if [ ! -f /etc/pki/tls/certs/localhost.crt ]; then + openssl req -newkey rsa:2048 -nodes \ + -keyout /etc/pki/tls/private/localhost.key \ + -x509 -days 365 \ + -out /etc/pki/tls/certs/localhost.crt \ + -subj '/CN=localhost' 2>/dev/null + fi + httpd -k start 2>/dev/null +} + +stop_httpd() { + httpd -k stop 2>/dev/null || true +} + +case "$action" in + start) + case "$unit" in + mysqld|mysql) start_mysqld ;; + httpd) start_httpd ;; + sendmail) exit 0 ;; # no-op: sendmail cannot run without systemd + firewalld) exit 0 ;; # no-op: firewalld not available in Docker + *) echo "systemctl shim: unsupported unit '$unit'" >&2; exit 1 ;; + esac ;; + stop) + case "$unit" in + mysqld|mysql) stop_mysqld ;; + httpd) stop_httpd ;; + *) exit 0 ;; # non-fatal for unknown units on uninstall + esac ;; + restart) + case "$unit" in + mysqld|mysql) stop_mysqld; sleep 1; start_mysqld ;; + httpd) httpd -k restart 2>/dev/null ;; + *) echo "systemctl shim: unsupported unit '$unit'" >&2; exit 1 ;; + esac ;; + is-active) + case "$unit" in + mysqld|mysql) mysqladmin ping --silent >/dev/null 2>&1 ;; + httpd) pgrep httpd >/dev/null 2>&1 ;; + *) exit 1 ;; + esac ;; + status) + case "$unit" in + mysqld|mysql) mysqladmin ping --silent >/dev/null 2>&1 && echo "active" || exit 3 ;; + httpd) pgrep httpd >/dev/null 2>&1 && echo "active" || exit 3 ;; + *) exit 3 ;; + esac ;; + enable|disable|daemon-reload|mask|unmask|is-enabled|reset-failed) + exit 0 ;; # no-op — we don't manage boot-time units + *) + echo "systemctl shim: unknown action '$action'" >&2; exit 1 ;; +esac +EOF +RUN chmod +x /usr/local/bin/systemctl + +# firewall-cmd, setsebool, and chcon need no-op shims: +# - firewalld cannot run in Docker Desktop (no nftables/iptables backend) +# - SELinux is not enforced in Docker containers on Windows (WSL2) +# Placing shims in /usr/local/bin/ ensures they take precedence over the real +# binaries in /usr/sbin/ when the setup script's PATH is evaluated. +RUN printf '#!/bin/bash\nexit 0\n' > /usr/local/bin/firewall-cmd && \ + chmod +x /usr/local/bin/firewall-cmd && \ + printf '#!/bin/bash\nexit 0\n' > /usr/local/bin/setsebool && \ + chmod +x /usr/local/bin/setsebool && \ + printf '#!/bin/bash\nexit 0\n' > /usr/local/bin/chcon && \ + chmod +x /usr/local/bin/chcon + +CMD ["/bin/bash"] diff --git a/tests/dockerfiles/Dockerfile.centos-stream-9 b/tests/dockerfiles/Dockerfile.centos-stream-9 new file mode 100644 index 0000000..de3d4da --- /dev/null +++ b/tests/dockerfiles/Dockerfile.centos-stream-9 @@ -0,0 +1,156 @@ +FROM quay.io/centos/centos:stream9 + +ENV container=docker + +# --allowerasing lets DNF swap curl-minimal (pre-installed) for full curl. +# cronie (crontab) and which are pre-installed on real RHEL/CentOS servers but +# absent from the minimal container image; add them so the setup script can +# install the backup cron job. +RUN dnf -y install --allowerasing curl wget sudo cronie which && \ + dnf clean all + +# MySQL uses native AIO by default, which fails on Docker's overlayfs driver. +RUN mkdir -p /etc/my.cnf.d && \ + printf '[mysqld]\ninnodb_use_native_aio=0\n' > /etc/my.cnf.d/docker.cnf + +# systemd cannot start in Docker Desktop for Windows (cgroup v2 unavailable). +# This shim replaces /usr/bin/systemctl so the setup script's +# `systemctl start/stop/restart/enable/disable/daemon-reload` calls work +# by managing processes directly instead. +RUN cat > /usr/local/bin/systemctl << 'EOF' +#!/bin/bash +# Minimal systemctl shim — handles the subset used by simplerisk-setup.sh +# on CentOS/RHEL without requiring a running systemd PID 1. + +# Strip --quiet / --system / other flags; find the action and unit name. +args=() +for arg in "$@"; do + [[ "$arg" == --* ]] && continue + args+=("$arg") +done +action="${args[0]:-}" +unit="${args[1]%.service}" # strip optional .service suffix + +start_mysqld() { + mysqladmin ping --silent >/dev/null 2>&1 && return 0 # already running + mkdir -p /var/run/mysqld && chown mysql:mysql /var/run/mysqld 2>/dev/null || true + # Pre-create the error log with mysql ownership so mysqld (running as mysql user) + # can open it for writing internally in addition to the shell redirect. + touch /var/log/mysqld.log && chown mysql:mysql /var/log/mysqld.log 2>/dev/null || true + # The RPM %post scriptlet may leave /var/lib/mysql in a partial state when + # systemd is unavailable (auto.cnf + binlog.index but no ibdata1). + # Re-initialize if ibdata1 is missing. + if [ ! -f /var/lib/mysql/ibdata1 ]; then + rm -f /var/lib/mysql/auto.cnf /var/lib/mysql/binlog.index 2>/dev/null || true + # Use --initialize (not --insecure) so a temporary root password is written + # to /var/log/mysqld.log as a "Note" line for simplerisk-setup.sh to read. + mysqld --initialize --user=mysql >>/var/log/mysqld.log 2>&1 + fi + # mysqld_safe and --daemonize were removed in MySQL 8.4; run as background process. + # Use --init-file to install the validate_password component at startup: + # the community RPM does not pre-install it when systemd is absent, but + # simplerisk-setup.sh calls SET GLOBAL validate_password.policy=LOW. + # Errors in --init-file are non-fatal, so this is safe on subsequent restarts + # when the component is already installed. + printf "INSTALL COMPONENT 'file://component_validate_password';\n" > /var/lib/mysql/docker-init.sql + # Append (>>) so the initialization "Note: temp password" line is preserved. + nohup mysqld --user=mysql --init-file=/var/lib/mysql/docker-init.sql >>/var/log/mysqld.log 2>&1 & + local i=0 + while [ $i -lt 60 ]; do + mysqladmin ping --silent >/dev/null 2>&1 && return 0 + sleep 1; i=$((i+1)) + done + echo "systemctl shim: mysqld did not start within 60s" >&2; return 1 +} + +stop_mysqld() { + # Send SIGTERM directly to the mysqld process so that no root password is + # needed (mysqladmin shutdown requires auth after setup changes the password). + local pidfile=/var/run/mysqld/mysqld.pid + if [ -f "$pidfile" ]; then + local pid + pid=$(cat "$pidfile" 2>/dev/null) + [ -n "$pid" ] && kill -TERM "$pid" 2>/dev/null || true + else + pkill -TERM mysqld 2>/dev/null || true + fi + # Wait for mysqld to fully stop before returning so that the subsequent + # start_mysqld call does not find MySQL still running and skip the restart. + local i=0 + while mysqladmin ping --silent >/dev/null 2>&1 && [ $i -lt 30 ]; do + sleep 1; i=$((i+1)) + done +} + +start_httpd() { + pgrep httpd >/dev/null 2>&1 && return 0 + # The mod_ssl %post scriptlet may skip cert generation without systemd. + # Generate a self-signed cert if missing so httpd can start. + if [ ! -f /etc/pki/tls/certs/localhost.crt ]; then + openssl req -newkey rsa:2048 -nodes \ + -keyout /etc/pki/tls/private/localhost.key \ + -x509 -days 365 \ + -out /etc/pki/tls/certs/localhost.crt \ + -subj '/CN=localhost' 2>/dev/null + fi + httpd -k start 2>/dev/null +} + +stop_httpd() { + httpd -k stop 2>/dev/null || true +} + +case "$action" in + start) + case "$unit" in + mysqld|mysql) start_mysqld ;; + httpd) start_httpd ;; + sendmail) exit 0 ;; # no-op: sendmail cannot run without systemd + firewalld) exit 0 ;; # no-op: firewalld not available in Docker + *) echo "systemctl shim: unsupported unit '$unit'" >&2; exit 1 ;; + esac ;; + stop) + case "$unit" in + mysqld|mysql) stop_mysqld ;; + httpd) stop_httpd ;; + *) exit 0 ;; # non-fatal for unknown units on uninstall + esac ;; + restart) + case "$unit" in + mysqld|mysql) stop_mysqld; sleep 1; start_mysqld ;; + httpd) httpd -k restart 2>/dev/null ;; + *) echo "systemctl shim: unsupported unit '$unit'" >&2; exit 1 ;; + esac ;; + is-active) + case "$unit" in + mysqld|mysql) mysqladmin ping --silent >/dev/null 2>&1 ;; + httpd) pgrep httpd >/dev/null 2>&1 ;; + *) exit 1 ;; + esac ;; + status) + case "$unit" in + mysqld|mysql) mysqladmin ping --silent >/dev/null 2>&1 && echo "active" || exit 3 ;; + httpd) pgrep httpd >/dev/null 2>&1 && echo "active" || exit 3 ;; + *) exit 3 ;; + esac ;; + enable|disable|daemon-reload|mask|unmask|is-enabled|reset-failed) + exit 0 ;; # no-op — we don't manage boot-time units + *) + echo "systemctl shim: unknown action '$action'" >&2; exit 1 ;; +esac +EOF +RUN chmod +x /usr/local/bin/systemctl + +# firewall-cmd, setsebool, and chcon need no-op shims: +# - firewalld cannot run in Docker Desktop (no nftables/iptables backend) +# - SELinux is not enforced in Docker containers on Windows (WSL2) +# Placing shims in /usr/local/bin/ ensures they take precedence over the real +# binaries in /usr/sbin/ when the setup script's PATH is evaluated. +RUN printf '#!/bin/bash\nexit 0\n' > /usr/local/bin/firewall-cmd && \ + chmod +x /usr/local/bin/firewall-cmd && \ + printf '#!/bin/bash\nexit 0\n' > /usr/local/bin/setsebool && \ + chmod +x /usr/local/bin/setsebool && \ + printf '#!/bin/bash\nexit 0\n' > /usr/local/bin/chcon && \ + chmod +x /usr/local/bin/chcon + +CMD ["/bin/bash"] diff --git a/tests/dockerfiles/Dockerfile.debian-12 b/tests/dockerfiles/Dockerfile.debian-12 new file mode 100644 index 0000000..19343e5 --- /dev/null +++ b/tests/dockerfiles/Dockerfile.debian-12 @@ -0,0 +1,100 @@ +FROM debian:12 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + sudo \ + curl \ + wget \ + lsb-release \ + ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +# MySQL uses native AIO by default, which fails on Docker's overlayfs storage driver. +RUN mkdir -p /etc/mysql/conf.d && \ + printf '[mysqld]\ninnodb_use_native_aio=0\n' > /etc/mysql/conf.d/docker.cnf + +# MySQL 8.4 community server on Debian ships only systemd units (no init.d script). +# This wrapper lets `service mysql start/stop/restart/status` work in a non-systemd +# container by invoking mysqld directly via the helpers bundled with the package. +RUN mkdir -p /etc/init.d && cat > /etc/init.d/mysql << 'EOF' +#!/bin/bash +### BEGIN INIT INFO +# Provides: mysql +# Required-Start: $local_fs $network +# Required-Stop: $local_fs +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: MySQL Community Server +# Description: MySQL Community Server wrapper for non-systemd containers +### END INIT INFO + +PIDFILE=/var/run/mysqld/mysqld.pid +MYSQLD=/usr/sbin/mysqld +HELPERS=/usr/share/mysql-8.4/mysql-helpers + +do_start() { + if [ ! -x "$MYSQLD" ]; then + echo "mysqld not found at $MYSQLD" >&2 + return 1 + fi + # Source helpers for verify_ready / verify_database / get_running + . "$HELPERS" + verify_ready "" + verify_database "" + if [ "$(get_running)" = "1" ]; then + echo "MySQL is already running" + return 0 + fi + mkdir -p /var/run/mysqld && chown mysql:mysql /var/run/mysqld + su -s /bin/bash mysql -c "$MYSQLD --daemonize --pid-file=$PIDFILE" 2>&1 + # Wait for MySQL to accept connections (up to 30s) + local i=0 + while [ $i -lt 30 ]; do + mysqladmin ping --silent >/dev/null 2>&1 && return 0 + sleep 1 + i=$((i+1)) + done + echo "MySQL did not start within 30 seconds" >&2 + return 1 +} + +do_stop() { + if [ -f "$PIDFILE" ]; then + local pid + pid=$(cat "$PIDFILE" 2>/dev/null) || return 0 + kill "$pid" 2>/dev/null || true + local i=0 + while [ -d "/proc/$pid" ] && [ $i -lt 20 ]; do + sleep 1 + i=$((i+1)) + done + rm -f "$PIDFILE" + fi + return 0 +} + +do_status() { + if [ ! -x "$MYSQLD" ]; then + return 3 + fi + . "$HELPERS" + if [ "$(get_running)" = "1" ]; then + return 0 + else + return 1 + fi +} + +case "$1" in + start) do_start ;; + stop) do_stop ;; + restart|force-reload) do_stop; sleep 1; do_start ;; + status) do_status ;; + *) echo "Usage: $0 {start|stop|restart|force-reload|status}"; exit 1 ;; +esac +EOF +RUN chmod +x /etc/init.d/mysql + +CMD ["/bin/bash"] diff --git a/tests/dockerfiles/Dockerfile.debian-13 b/tests/dockerfiles/Dockerfile.debian-13 new file mode 100644 index 0000000..368bfda --- /dev/null +++ b/tests/dockerfiles/Dockerfile.debian-13 @@ -0,0 +1,100 @@ +FROM debian:13 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + sudo \ + curl \ + wget \ + lsb-release \ + ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +# MySQL uses native AIO by default, which fails on Docker's overlayfs storage driver. +RUN mkdir -p /etc/mysql/conf.d && \ + printf '[mysqld]\ninnodb_use_native_aio=0\n' > /etc/mysql/conf.d/docker.cnf + +# MySQL 8.4 community server on Debian ships only systemd units (no init.d script). +# This wrapper lets `service mysql start/stop/restart/status` work in a non-systemd +# container by invoking mysqld directly via the helpers bundled with the package. +RUN mkdir -p /etc/init.d && cat > /etc/init.d/mysql << 'EOF' +#!/bin/bash +### BEGIN INIT INFO +# Provides: mysql +# Required-Start: $local_fs $network +# Required-Stop: $local_fs +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: MySQL Community Server +# Description: MySQL Community Server wrapper for non-systemd containers +### END INIT INFO + +PIDFILE=/var/run/mysqld/mysqld.pid +MYSQLD=/usr/sbin/mysqld +HELPERS=/usr/share/mysql-8.4/mysql-helpers + +do_start() { + if [ ! -x "$MYSQLD" ]; then + echo "mysqld not found at $MYSQLD" >&2 + return 1 + fi + # Source helpers for verify_ready / verify_database / get_running + . "$HELPERS" + verify_ready "" + verify_database "" + if [ "$(get_running)" = "1" ]; then + echo "MySQL is already running" + return 0 + fi + mkdir -p /var/run/mysqld && chown mysql:mysql /var/run/mysqld + su -s /bin/bash mysql -c "$MYSQLD --daemonize --pid-file=$PIDFILE" 2>&1 + # Wait for MySQL to accept connections (up to 30s) + local i=0 + while [ $i -lt 30 ]; do + mysqladmin ping --silent >/dev/null 2>&1 && return 0 + sleep 1 + i=$((i+1)) + done + echo "MySQL did not start within 30 seconds" >&2 + return 1 +} + +do_stop() { + if [ -f "$PIDFILE" ]; then + local pid + pid=$(cat "$PIDFILE" 2>/dev/null) || return 0 + kill "$pid" 2>/dev/null || true + local i=0 + while [ -d "/proc/$pid" ] && [ $i -lt 20 ]; do + sleep 1 + i=$((i+1)) + done + rm -f "$PIDFILE" + fi + return 0 +} + +do_status() { + if [ ! -x "$MYSQLD" ]; then + return 3 + fi + . "$HELPERS" + if [ "$(get_running)" = "1" ]; then + return 0 + else + return 1 + fi +} + +case "$1" in + start) do_start ;; + stop) do_stop ;; + restart|force-reload) do_stop; sleep 1; do_start ;; + status) do_status ;; + *) echo "Usage: $0 {start|stop|restart|force-reload|status}"; exit 1 ;; +esac +EOF +RUN chmod +x /etc/init.d/mysql + +CMD ["/bin/bash"] diff --git a/tests/dockerfiles/Dockerfile.ubuntu-22.04 b/tests/dockerfiles/Dockerfile.ubuntu-22.04 new file mode 100644 index 0000000..fa42ac9 --- /dev/null +++ b/tests/dockerfiles/Dockerfile.ubuntu-22.04 @@ -0,0 +1,44 @@ +FROM ubuntu:22.04 + +ENV DEBIAN_FRONTEND=noninteractive +ENV container=docker + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + sudo \ + curl \ + wget \ + lsb-release \ + ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +# MySQL uses native AIO by default, which fails on Docker's overlayfs storage +# driver during the post-install data directory initialisation. Pre-creating +# this config disables AIO so that `lamp-server^` installs cleanly. +RUN mkdir -p /etc/mysql/conf.d && \ + printf '[mysqld]\ninnodb_use_native_aio=0\n' > /etc/mysql/conf.d/docker.cnf + +# DENY all service operations during the lamp-server^ pre-install below. +# This is the standard Docker pattern: MySQL's post-install script tries to +# start mysqld for initialization and then shut it down — the shutdown step +# times out inside Docker's overlayfs because the process can't be signalled +# cleanly. By denying service actions here, dpkg skips start/stop but still +# initialises the data directory via mysqld --initialize, which is sufficient. +RUN printf '#!/bin/sh\nexit 101\n' > /usr/sbin/policy-rc.d && \ + chmod +x /usr/sbin/policy-rc.d + +# Pre-install lamp-server^ so that when the setup script calls +# `apt-get install -y lamp-server^` during the test run, all packages are +# already present and dpkg has nothing to reconfigure. +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y lamp-server^ && \ + rm -rf /var/lib/apt/lists/* + +# Switch back to ALLOW so the setup script can start/stop services normally +# during the test (service mysql restart, service apache2 restart, etc.). +RUN printf '#!/bin/sh\nexit 0\n' > /usr/sbin/policy-rc.d && \ + chmod +x /usr/sbin/policy-rc.d + +# No systemd required — the script uses `service` on Ubuntu/Debian, +# which invokes /etc/init.d wrappers and works in containers. +CMD ["/bin/bash"] diff --git a/tests/dockerfiles/Dockerfile.ubuntu-24.04 b/tests/dockerfiles/Dockerfile.ubuntu-24.04 new file mode 100644 index 0000000..ea8c31d --- /dev/null +++ b/tests/dockerfiles/Dockerfile.ubuntu-24.04 @@ -0,0 +1,35 @@ +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive +ENV container=docker + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + sudo \ + curl \ + wget \ + lsb-release \ + ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +# MySQL uses native AIO by default, which fails on Docker's overlayfs storage +# driver during the post-install data directory initialisation. +RUN mkdir -p /etc/mysql/conf.d && \ + printf '[mysqld]\ninnodb_use_native_aio=0\n' > /etc/mysql/conf.d/docker.cnf + +# DENY all service operations during the lamp-server^ pre-install below. +# MySQL's post-install shutdown times out inside Docker; denying start/stop +# lets dpkg initialise the data directory without triggering the timeout. +RUN printf '#!/bin/sh\nexit 101\n' > /usr/sbin/policy-rc.d && \ + chmod +x /usr/sbin/policy-rc.d + +# Pre-install lamp-server^ so packages are already present for the test run. +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y lamp-server^ && \ + rm -rf /var/lib/apt/lists/* + +# Switch back to ALLOW so the setup script can start/stop services normally. +RUN printf '#!/bin/sh\nexit 0\n' > /usr/sbin/policy-rc.d && \ + chmod +x /usr/sbin/policy-rc.d + +CMD ["/bin/bash"] diff --git a/tests/run-tests.sh b/tests/run-tests.sh new file mode 100644 index 0000000..6d63dc9 --- /dev/null +++ b/tests/run-tests.sh @@ -0,0 +1,243 @@ +#!/usr/bin/env bash +# run-tests.sh — Local test orchestrator for simplerisk-setup.sh +# +# Usage: +# ./tests/run-tests.sh # Run all OSes +# ./tests/run-tests.sh ubuntu-22.04 # Run a single OS +# +# Available OS slugs: +# ubuntu-22.04 ubuntu-24.04 +# debian-12 debian-13 +# centos-stream-9 centos-stream-10 +# +# Requirements: +# - Docker must be installed and running +# - Outbound internet access (the setup script downloads packages from the web) + +set -euo pipefail + +# On Windows/Git Bash, paths like /bin/bash and /root/... that are meant to +# be resolved *inside* the container get incorrectly converted to Windows paths +# by Git Bash before Docker sees them. Wrapping docker run/exec/cp calls with +# this function suppresses that conversion for those specific invocations. +# docker build still runs without it so that host build-context paths resolve correctly. +docker_nc() { MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL="*" docker "$@"; } + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +LOG_BASE="$SCRIPT_DIR/logs" + +# All supported test targets +ALL_OS_SLUGS=( + ubuntu-22.04 + ubuntu-24.04 + debian-12 + debian-13 + centos-stream-9 + centos-stream-10 +) + +# ── Helpers ─────────────────────────────────────────────────────────────────── +log() { echo "[$(date '+%H:%M:%S')] $*"; } +info() { echo ""; log ">>> $*"; echo ""; } +pass() { echo ""; echo " ✔ $*"; echo ""; } +fail() { echo ""; echo " ✘ $*"; echo ""; } + +uses_systemd() { + local slug="$1" + # Docker Desktop for Windows does not expose a real cgroup hierarchy to + # containers, so systemd (which needs cgroup v2 or legacy v1 mounts) cannot + # start as PID 1 in any of our test containers. All OSes use the non-systemd + # path: --init + tail -f /dev/null, with service wrappers baked into each + # Dockerfile (an /etc/init.d/mysql script for Debian, a /usr/bin/systemctl + # shim for CentOS/RHEL). + # Keep this function in case a future CI environment re-enables systemd. + false +} + +# ── Single OS test ───────────────────────────────────────────────────────────── +run_test() { + local OS_SLUG="$1" + local DOCKERFILE="$SCRIPT_DIR/dockerfiles/Dockerfile.${OS_SLUG}" + local IMAGE_NAME="simplerisk-test:${OS_SLUG}" + local CONTAINER_NAME="simplerisk-test-${OS_SLUG//./-}" + local LOG_DIR="$LOG_BASE/${OS_SLUG}" + + if [ ! -f "$DOCKERFILE" ]; then + echo "ERROR: Dockerfile not found: $DOCKERFILE" + return 1 + fi + + mkdir -p "$LOG_DIR" + + # Remove any leftover container from a previous failed run. + docker_nc rm -f "$CONTAINER_NAME" > /dev/null 2>&1 || true + + # Always clean up the container on exit, even on error. + # Guard against CONTAINER_NAME being unset if docker run fails early. + cleanup() { + if [ -n "${CONTAINER_NAME:-}" ]; then + log "Teardown: removing container $CONTAINER_NAME" + docker_nc rm -f "$CONTAINER_NAME" > /dev/null 2>&1 || true + fi + } + trap cleanup EXIT + + echo "==========================================" + echo " SimpleRisk Test: $OS_SLUG" + echo "==========================================" + + # ── Step 1: Build ──────────────────────────────────────────────────────── + info "Step 1/7 — Building image: $IMAGE_NAME" + docker build \ + -f "$DOCKERFILE" \ + -t "$IMAGE_NAME" \ + "$SCRIPT_DIR/dockerfiles" \ + 2>&1 | tee "$LOG_DIR/build.log" + + # ── Step 2: Start container ────────────────────────────────────────────── + info "Step 2/7 — Starting container: $CONTAINER_NAME" + + if uses_systemd "$OS_SLUG"; then + # CentOS/RHEL: systemd must be PID 1. + # --privileged is sufficient on Docker Desktop (Windows/Mac) and on + # Linux CI runners; the cgroup hierarchy is managed by the host VM. + docker_nc run -d \ + --name "$CONTAINER_NAME" \ + --privileged \ + --cgroupns=host \ + --tmpfs /run \ + --tmpfs /run/lock \ + "$IMAGE_NAME" \ + 2>&1 | tee "$LOG_DIR/start.log" + + log "Waiting for systemd multi-user.target..." + local attempts=0 + until docker_nc exec "$CONTAINER_NAME" \ + systemctl is-active --quiet multi-user.target 2>/dev/null; do + sleep 3 + attempts=$((attempts + 1)) + if [ "$attempts" -ge 30 ]; then + echo "ERROR: systemd did not reach multi-user.target within 90s" + docker_nc exec "$CONTAINER_NAME" journalctl -n 50 2>/dev/null || true + return 1 + fi + done + log "systemd ready." + else + # Ubuntu/Debian: no systemd needed. + # --privileged is required for ufw/iptables inside the container. + # --init uses Docker's built-in tini as PID 1 so that child processes + # (Apache workers, etc.) are properly reaped and do not accumulate as + # zombies across the many service start/stop/restart calls the script makes. + docker_nc run -d \ + --name "$CONTAINER_NAME" \ + --privileged \ + --init \ + "$IMAGE_NAME" \ + /bin/bash -c "tail -f /dev/null" \ + 2>&1 | tee "$LOG_DIR/start.log" + sleep 1 + + # Ubuntu: lamp-server^ was pre-installed in the Dockerfile (with service + # starts denied via policy-rc.d) to speed up test runs. On a real host + # the post-install scripts would leave MySQL and Apache running; replicate + # that state here so the setup script finds services in the expected state. + # Debian: packages are NOT pre-installed; these start calls will fail fast + # and harmlessly (|| true) — the setup script installs and starts everything. + log "Starting pre-installed services (mysql, apache2) if present..." + docker_nc exec "$CONTAINER_NAME" service mysql start 2>&1 | tee -a "$LOG_DIR/start.log" || true + docker_nc exec "$CONTAINER_NAME" service apache2 start 2>&1 | tee -a "$LOG_DIR/start.log" || true + fi + + # ── Step 3: Copy files into container ──────────────────────────────────── + # Use plain `docker cp` here: Git Bash correctly converts the host source + # path, and does not mangle `container:/dest` arguments (the colon guards it). + info "Step 3/7 — Copying scripts into container" + docker cp "$REPO_ROOT/simplerisk-setup.sh" "$CONTAINER_NAME:/root/simplerisk-setup.sh" + docker cp "$SCRIPT_DIR/verify-install.sh" "$CONTAINER_NAME:/root/verify-install.sh" + docker cp "$SCRIPT_DIR/verify-uninstall.sh" "$CONTAINER_NAME:/root/verify-uninstall.sh" + # Strip Windows CRLF line endings that Git on Windows may have introduced, + # then make the scripts executable. + docker_nc exec "$CONTAINER_NAME" \ + sed -i 's/\r//' /root/simplerisk-setup.sh /root/verify-install.sh /root/verify-uninstall.sh + docker_nc exec "$CONTAINER_NAME" chmod +x \ + /root/simplerisk-setup.sh \ + /root/verify-install.sh \ + /root/verify-uninstall.sh + + # ── Step 4: Install ────────────────────────────────────────────────────── + info "Step 4/7 — Running install (--yes --debug)" + if ! docker_nc exec "$CONTAINER_NAME" \ + bash /root/simplerisk-setup.sh --yes --debug \ + 2>&1 | tee "$LOG_DIR/install.log"; then + echo "ERROR: Install script failed. See $LOG_DIR/install.log" + return 1 + fi + + # ── Step 5: Verify install ─────────────────────────────────────────────── + info "Step 5/7 — Verifying installation" + if ! docker_nc exec "$CONTAINER_NAME" \ + bash /root/verify-install.sh \ + 2>&1 | tee "$LOG_DIR/verify-install.log"; then + echo "ERROR: Install verification failed. See $LOG_DIR/verify-install.log" + return 1 + fi + + # ── Step 6: Uninstall ──────────────────────────────────────────────────── + info "Step 6/7 — Running uninstall (--uninstall --yes --debug)" + if ! docker_nc exec "$CONTAINER_NAME" \ + bash /root/simplerisk-setup.sh --uninstall --yes --debug \ + 2>&1 | tee "$LOG_DIR/uninstall.log"; then + echo "ERROR: Uninstall script failed. See $LOG_DIR/uninstall.log" + return 1 + fi + + # ── Step 7: Verify uninstall ───────────────────────────────────────────── + info "Step 7/7 — Verifying uninstallation" + if ! docker_nc exec "$CONTAINER_NAME" \ + bash /root/verify-uninstall.sh \ + 2>&1 | tee "$LOG_DIR/verify-uninstall.log"; then + echo "ERROR: Uninstall verification failed. See $LOG_DIR/verify-uninstall.log" + return 1 + fi + + pass "ALL TESTS PASSED: $OS_SLUG" + return 0 +} + +# ── Entry point ─────────────────────────────────────────────────────────────── +TARGET="${1:-}" + +if [ -n "$TARGET" ]; then + # Run a single OS + run_test "$TARGET" +else + # Run all OSes; collect results + RESULTS_PASS=() + RESULTS_FAIL=() + + for slug in "${ALL_OS_SLUGS[@]}"; do + if run_test "$slug"; then + RESULTS_PASS+=("$slug") + else + RESULTS_FAIL+=("$slug") + fi + done + + echo "" + echo "==========================================" + echo " Final Results" + echo "==========================================" + for s in "${RESULTS_PASS[@]:-}"; do echo " PASS $s"; done + for s in "${RESULTS_FAIL[@]:-}"; do echo " FAIL $s"; done + echo "" + + if [ "${#RESULTS_FAIL[@]}" -gt 0 ]; then + echo " ${#RESULTS_FAIL[@]} of ${#ALL_OS_SLUGS[@]} test(s) FAILED." + exit 1 + fi + + echo " All ${#ALL_OS_SLUGS[@]} tests passed." + exit 0 +fi diff --git a/tests/verify-install.sh b/tests/verify-install.sh new file mode 100644 index 0000000..4721ace --- /dev/null +++ b/tests/verify-install.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# verify-install.sh — Run inside a container after simplerisk-setup.sh --yes +# to assert that the installation completed correctly. +# Exits 0 if all checks pass, 1 if any check fails. + +set -uo pipefail + +PASS=0 +FAIL=0 +ERRORS=() + +# check [args...] +# Runs the command silently; records pass/fail without aborting on failure. +check() { + local description="$1" + shift + local result=0 + "$@" > /dev/null 2>&1 || result=$? + if [ "$result" -eq 0 ]; then + echo " PASS: $description" + PASS=$((PASS + 1)) + else + echo " FAIL: $description" + ERRORS+=("$description") + FAIL=$((FAIL + 1)) + fi +} + +echo "=== SimpleRisk Install Verification ===" +echo "" + +# ── File system ────────────────────────────────────────────────────────────── +echo "--- File system ---" +check "SimpleRisk directory exists" test -d /var/www/simplerisk +check "index.php exists" test -f /var/www/simplerisk/index.php +check "config.php exists" test -f /var/www/simplerisk/includes/config.php +check "database.sql was removed post-install" test ! -f /var/www/simplerisk/database.sql +check "cron script exists" test -f /var/www/simplerisk/cron/cron.php + +# ── config.php content ─────────────────────────────────────────────────────── +echo "--- config.php ---" +check "SIMPLERISK_INSTALLED is set to true" \ + grep -q "SIMPLERISK_INSTALLED', 'true'" /var/www/simplerisk/includes/config.php +check "DB_PASSWORD is not the default literal 'simplerisk'" \ + bash -c "! grep -q \"DB_PASSWORD', 'simplerisk'\" /var/www/simplerisk/includes/config.php" + +# ── Passwords file ─────────────────────────────────────────────────────────── +echo "--- /root/passwords.txt ---" +check "passwords.txt exists" test -f /root/passwords.txt +check "passwords.txt has mode 600" \ + bash -c '[ "$(stat -c %a /root/passwords.txt)" = "600" ]' +check "passwords.txt contains MySQL root password entry" \ + grep -q "MYSQL ROOT PASSWORD:" /root/passwords.txt +check "passwords.txt contains MySQL simplerisk password entry" \ + grep -q "MYSQL SIMPLERISK PASSWORD:" /root/passwords.txt + +MYSQL_ROOT_PW=$(grep "MYSQL ROOT PASSWORD:" /root/passwords.txt 2>/dev/null | awk -F ': ' '{print $2}') +MYSQL_SR_PW=$(grep "MYSQL SIMPLERISK PASSWORD:" /root/passwords.txt 2>/dev/null | awk -F ': ' '{print $2}') + +check "MySQL root password is non-empty" test -n "${MYSQL_ROOT_PW:-}" +check "MySQL simplerisk password is non-empty" test -n "${MYSQL_SR_PW:-}" + +# ── MySQL ──────────────────────────────────────────────────────────────────── +echo "--- MySQL ---" +check "MySQL is reachable with root password" \ + mysql -uroot --password="${MYSQL_ROOT_PW:-}" -e "SELECT 1;" +check "simplerisk database exists" \ + bash -c "mysql -uroot --password='${MYSQL_ROOT_PW:-}' -e 'SHOW DATABASES;' 2>/dev/null | grep -q simplerisk" +check "simplerisk user can connect" \ + mysql -usimplerisk --password="${MYSQL_SR_PW:-}" simplerisk -e "SELECT 1;" +check "sql_mode does not contain STRICT_TRANS_TABLES" \ + bash -c "! mysql -uroot --password='${MYSQL_ROOT_PW:-}' -e 'SELECT @@sql_mode;' 2>/dev/null | grep -q STRICT_TRANS_TABLES" + +# ── Cron job ───────────────────────────────────────────────────────────────── +echo "--- Cron ---" +check "Backup cron job is in root's crontab" \ + bash -c "crontab -l 2>/dev/null | grep -q 'simplerisk/cron/cron.php'" + +# ── PHP ────────────────────────────────────────────────────────────────────── +echo "--- PHP ---" +check "PHP CLI is functional" php -r "echo 'OK';" +check "PHP version is 8.x" bash -c "php --version | grep -qE '^PHP 8\.'" + +for ext in mysqli mbstring xml curl gd zip intl ldap; do + check "PHP extension '$ext' is loaded" php -m | grep -qi "$ext" +done + +# ── Web server (OS-conditional) ─────────────────────────────────────────────── +echo "--- Web server ---" +if command -v apache2 > /dev/null 2>&1; then + # Debian/Ubuntu + # apache2 -t requires env vars (APACHE_RUN_DIR etc.) that are only set by + # apache2ctl. Use apache2ctl -t so the environment is populated correctly. + check "Apache config syntax is valid" bash -c "apache2ctl -t 2>&1 | grep -q 'Syntax OK'" + check "Apache service is running" service apache2 status + check "MySQL service is running" bash -c "service mysql status 2>/dev/null || service mysqld status 2>/dev/null" +elif command -v httpd > /dev/null 2>&1; then + # CentOS/RHEL + check "Apache config syntax is valid" bash -c "httpd -t 2>&1 | grep -q 'Syntax OK'" + check "httpd service is running (systemctl)" systemctl is-active --quiet httpd + check "mysqld service is running (systemctl)" systemctl is-active --quiet mysqld +fi + +# ── HTTP reachability ──────────────────────────────────────────────────────── +echo "--- HTTP ---" +check "HTTP on port 80 returns a redirect or 200" \ + bash -c "curl -sk -o /dev/null -w '%{http_code}' http://localhost/ | grep -qE '^(200|301|302)$'" + +# ── Summary ────────────────────────────────────────────────────────────────── +echo "" +echo "=== Verification Summary ===" +echo " Passed : $PASS" +echo " Failed : $FAIL" + +if [ "$FAIL" -gt 0 ]; then + echo "" + echo " Failed checks:" + for err in "${ERRORS[@]}"; do + echo " - $err" + done + exit 1 +fi + +echo " All checks passed." +exit 0 diff --git a/tests/verify-uninstall.sh b/tests/verify-uninstall.sh new file mode 100644 index 0000000..4c8d90d --- /dev/null +++ b/tests/verify-uninstall.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# verify-uninstall.sh — Run inside a container after simplerisk-setup.sh --uninstall --yes +# to assert that all SimpleRisk components were cleanly removed. +# Exits 0 if all checks pass, 1 if any check fails. + +set -uo pipefail + +PASS=0 +FAIL=0 +ERRORS=() + +# check [args...] +check() { + local description="$1" + shift + local result=0 + "$@" > /dev/null 2>&1 || result=$? + if [ "$result" -eq 0 ]; then + echo " PASS: $description" + PASS=$((PASS + 1)) + else + echo " FAIL: $description" + ERRORS+=("$description") + FAIL=$((FAIL + 1)) + fi +} + +echo "=== SimpleRisk Uninstall Verification ===" +echo "" + +# ── File system ────────────────────────────────────────────────────────────── +echo "--- File system ---" +check "SimpleRisk directory was removed" test ! -d /var/www/simplerisk +check "passwords.txt was removed" test ! -f /root/passwords.txt + +# ── Cron job ───────────────────────────────────────────────────────────────── +echo "--- Cron ---" +check "Backup cron job was removed from root's crontab" \ + bash -c "! crontab -l 2>/dev/null | grep -q 'simplerisk/cron/cron.php'" + +# ── MySQL database ─────────────────────────────────────────────────────────── +echo "--- MySQL database ---" +if command -v mysql > /dev/null 2>&1; then + # MySQL client still installed (partially removed or not purged) — attempt + # socket auth as root and verify the database is gone. + check "simplerisk database was dropped" \ + bash -c "! mysql -uroot 2>/dev/null -e 'USE simplerisk;'" + check "simplerisk MySQL user was dropped" \ + bash -c "! mysql -uroot 2>/dev/null -e \"SELECT User FROM mysql.user WHERE User='simplerisk';\" | grep -q simplerisk" +else + echo " PASS: mysql client not present (packages removed)" + PASS=$((PASS + 1)) +fi + +# ── OS-specific package and config checks ──────────────────────────────────── +if grep -qi ubuntu /etc/os-release 2>/dev/null || grep -qi debian /etc/os-release 2>/dev/null; then + + echo "--- Packages (Debian/Ubuntu) ---" + check "apache2 package is removed" \ + bash -c "! dpkg -l apache2 2>/dev/null | grep -q '^ii'" + check "mysql-server package is removed" \ + bash -c "! dpkg -l mysql-server 2>/dev/null | grep -q '^ii'" + check "PHP packages are removed" \ + bash -c "! dpkg -l 'php*' 2>/dev/null | grep -qE '^ii.*php[0-9]'" + check "sendmail package is removed" \ + bash -c "! dpkg -l sendmail 2>/dev/null | grep -q '^ii'" + + if grep -qi debian /etc/os-release 2>/dev/null; then + echo "--- Repositories (Debian) ---" + check "sury-php.list was removed" \ + test ! -f /etc/apt/sources.list.d/sury-php.list + check "mysql.list was removed" \ + test ! -f /etc/apt/sources.list.d/mysql.list + check "sury-php GPG key was removed" \ + test ! -f /etc/apt/keyrings/sury-php.gpg + check "MySQL GPG key was removed" \ + test ! -f /etc/apt/trusted.gpg.d/mysql.gpg + fi + +elif grep -qi "centos\|red hat" /etc/os-release 2>/dev/null; then + + echo "--- Packages (CentOS/RHEL) ---" + check "httpd package is removed" \ + bash -c "! rpm -q httpd" + check "mysql-community-server package is removed" \ + bash -c "! rpm -q mysql-community-server" + check "PHP packages are removed" \ + bash -c "! rpm -qa 'php*' | grep -q ." + + echo "--- Services (CentOS/RHEL) ---" + check "httpd service is not active" \ + bash -c "! systemctl is-active --quiet httpd 2>/dev/null" + check "mysqld service is not active" \ + bash -c "! systemctl is-active --quiet mysqld 2>/dev/null" + + echo "--- Config files (CentOS/RHEL) ---" + check "simplerisk Apache vhost config was removed" \ + test ! -f /etc/httpd/sites-enabled/simplerisk.conf + +fi + +# ── Summary ────────────────────────────────────────────────────────────────── +echo "" +echo "=== Uninstall Verification Summary ===" +echo " Passed : $PASS" +echo " Failed : $FAIL" + +if [ "$FAIL" -gt 0 ]; then + echo "" + echo " Failed checks:" + for err in "${ERRORS[@]}"; do + echo " - $err" + done + exit 1 +fi + +echo " All checks passed." +exit 0 From 87d4e38289f4cac13211f010ef9c1adb97911b8b Mon Sep 17 00:00:00 2001 From: DASimp Date: Fri, 8 May 2026 12:58:57 -0500 Subject: [PATCH 2/5] Fix CI: replace Dockerfile heredocs with COPY'd scripts, add --init, fix CentOS start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three CI failures fixed: 1. Dockerfile heredoc parse error (Debian-12, Debian-13, CentOS-9, CentOS-10) Docker's standard Dockerfile parser treats `RUN cat > /file << 'EOF'` as a single-line RUN — subsequent lines are parsed as new Dockerfile instructions, causing "unknown instruction: PIDFILE=..." errors on the Linux CI runner. Fix: extract the mysql init script and systemctl shim to separate files (mysql-init-debian.sh, systemctl-shim-centos.sh) and use COPY + chmod. Also add eol=lf to .gitattributes for *.sh, Dockerfile*, and *.yml so CRLF conversion on Windows never affects files parsed on Linux. 2. Ubuntu Apache restart timeout during apt post-install triggers The libapache2-mod-php post-install script fires `service apache2 restart` multiple times in quick succession. Without --init (tini as PID 1), child processes are not properly reaped and a graceful restart can hang for the full 60s timeout. Fix: add --init to all docker run commands in the CI workflow (matches what run-tests.sh already does locally). 3. CentOS CI container start used the systemd path The CI workflow started CentOS containers with --cgroupns=host / --tmpfs expecting systemd as PID 1, then waited for multi-user.target. But the Dockerfiles use the /usr/local/bin/systemctl shim (no real systemd). Fix: unify container start for all OSes to --init + tail -f /dev/null; remove the separate CentOS systemd-start step and the "Wait for systemd" step entirely. Also adds a "Start pre-installed services" step in CI for Ubuntu only, to mirror what run-tests.sh does locally: start MySQL and Apache before the setup script runs, since Ubuntu images pre-install lamp-server^ during the Docker build (services blocked by policy-rc.d). Co-Authored-By: Claude Sonnet 4.6 --- .gitattributes | 6 + .github/workflows/install-test.yml | 55 ++++---- tests/dockerfiles/Dockerfile.centos-stream-10 | 125 +----------------- tests/dockerfiles/Dockerfile.centos-stream-9 | 125 +----------------- tests/dockerfiles/Dockerfile.debian-12 | 80 +---------- tests/dockerfiles/Dockerfile.debian-13 | 80 +---------- tests/dockerfiles/mysql-init-debian.sh | 75 +++++++++++ tests/dockerfiles/systemctl-shim-centos.sh | 120 +++++++++++++++++ 8 files changed, 236 insertions(+), 430 deletions(-) create mode 100644 tests/dockerfiles/mysql-init-debian.sh create mode 100644 tests/dockerfiles/systemctl-shim-centos.sh diff --git a/.gitattributes b/.gitattributes index dfe0770..97ba646 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,8 @@ # Auto detect text files and perform LF normalization * text=auto + +# Enforce LF line endings for files that run inside Linux containers or are +# parsed by tools that are sensitive to CRLF (Docker, bash, etc.). +*.sh text eol=lf +Dockerfile* text eol=lf +*.yml text eol=lf diff --git a/.github/workflows/install-test.yml b/.github/workflows/install-test.yml index 8182ec3..aeb356a 100644 --- a/.github/workflows/install-test.yml +++ b/.github/workflows/install-test.yml @@ -39,45 +39,36 @@ jobs: -t simplerisk-test:${{ matrix.os-slug }} \ tests/dockerfiles/ - # ── Start — Debian/Ubuntu (no systemd needed) ─────────────────────────── - - name: Start container (Debian/Ubuntu) - if: ${{ !startsWith(matrix.os-slug, 'centos') }} + # ── Start container ───────────────────────────────────────────────────── + # All OSes use --init (tini as PID 1) so that child processes (Apache + # workers, mysqld) are properly reaped and multiple rapid service + # start/stop/restart calls during apt/dnf post-install triggers work + # correctly. CentOS/RHEL uses the /usr/local/bin/systemctl shim baked + # into the image; no real systemd is needed. + # --privileged is required for ufw/iptables inside Debian/Ubuntu and + # is harmless for CentOS. + - name: Start container run: | docker run -d \ --name simplerisk-test-${{ matrix.os-slug }} \ --privileged \ + --init \ simplerisk-test:${{ matrix.os-slug }} \ /bin/bash -c "tail -f /dev/null" - - # ── Start — CentOS/RHEL (systemd as PID 1) ───────────────────────────── - - name: Start container (CentOS/RHEL — systemd) - if: ${{ startsWith(matrix.os-slug, 'centos') }} + sleep 2 + + # ── Pre-start services (Ubuntu only) ──────────────────────────────────── + # Ubuntu images pre-install lamp-server^ during the Docker build (with + # policy-rc.d blocking auto-start). Start MySQL and Apache now so that + # the setup script finds them in the expected running state — mirroring + # what would happen on a freshly provisioned real server where the + # package post-install scripts start the services. + - name: Start pre-installed services (Ubuntu only) + if: ${{ startsWith(matrix.os-slug, 'ubuntu') }} run: | - docker run -d \ - --name simplerisk-test-${{ matrix.os-slug }} \ - --privileged \ - --cgroupns=host \ - --tmpfs /run \ - --tmpfs /run/lock \ - simplerisk-test:${{ matrix.os-slug }} - - - name: Wait for systemd (CentOS/RHEL only) - if: ${{ startsWith(matrix.os-slug, 'centos') }} - run: | - CONTAINER="simplerisk-test-${{ matrix.os-slug }}" - echo "Waiting for systemd multi-user.target..." - for i in $(seq 1 30); do - if docker exec "$CONTAINER" systemctl is-active --quiet multi-user.target 2>/dev/null; then - echo "systemd ready." - break - fi - sleep 3 - if [ "$i" -eq 30 ]; then - echo "ERROR: systemd did not reach multi-user.target within 90s" - docker exec "$CONTAINER" journalctl -n 100 2>/dev/null || true - exit 1 - fi - done + docker exec simplerisk-test-${{ matrix.os-slug }} service mysql start 2>/dev/null || true + docker exec simplerisk-test-${{ matrix.os-slug }} service apache2 start 2>/dev/null || true + sleep 2 # ── Copy files ────────────────────────────────────────────────────────── - name: Copy scripts into container diff --git a/tests/dockerfiles/Dockerfile.centos-stream-10 b/tests/dockerfiles/Dockerfile.centos-stream-10 index 84d226c..e56f841 100644 --- a/tests/dockerfiles/Dockerfile.centos-stream-10 +++ b/tests/dockerfiles/Dockerfile.centos-stream-10 @@ -17,128 +17,9 @@ RUN mkdir -p /etc/my.cnf.d && \ # This shim replaces /usr/bin/systemctl so the setup script's # `systemctl start/stop/restart/enable/disable/daemon-reload` calls work # by managing processes directly instead. -RUN cat > /usr/local/bin/systemctl << 'EOF' -#!/bin/bash -# Minimal systemctl shim — handles the subset used by simplerisk-setup.sh -# on CentOS/RHEL without requiring a running systemd PID 1. - -# Strip --quiet / --system / other flags; find the action and unit name. -args=() -for arg in "$@"; do - [[ "$arg" == --* ]] && continue - args+=("$arg") -done -action="${args[0]:-}" -unit="${args[1]%.service}" # strip optional .service suffix - -start_mysqld() { - mysqladmin ping --silent >/dev/null 2>&1 && return 0 # already running - mkdir -p /var/run/mysqld && chown mysql:mysql /var/run/mysqld 2>/dev/null || true - # Pre-create the error log with mysql ownership so mysqld (running as mysql user) - # can open it for writing internally in addition to the shell redirect. - touch /var/log/mysqld.log && chown mysql:mysql /var/log/mysqld.log 2>/dev/null || true - # The RPM %post scriptlet may leave /var/lib/mysql in a partial state when - # systemd is unavailable (auto.cnf + binlog.index but no ibdata1). - # Re-initialize if ibdata1 is missing. - if [ ! -f /var/lib/mysql/ibdata1 ]; then - rm -f /var/lib/mysql/auto.cnf /var/lib/mysql/binlog.index 2>/dev/null || true - # Use --initialize (not --insecure) so a temporary root password is written - # to /var/log/mysqld.log as a "Note" line for simplerisk-setup.sh to read. - mysqld --initialize --user=mysql >>/var/log/mysqld.log 2>&1 - fi - # mysqld_safe and --daemonize were removed in MySQL 8.4; run as background process. - # Use --init-file to install the validate_password component at startup: - # the community RPM does not pre-install it when systemd is absent, but - # simplerisk-setup.sh calls SET GLOBAL validate_password.policy=LOW. - # Errors in --init-file are non-fatal, so this is safe on subsequent restarts - # when the component is already installed. - printf "INSTALL COMPONENT 'file://component_validate_password';\n" > /var/lib/mysql/docker-init.sql - # Append (>>) so the initialization "Note: temp password" line is preserved. - nohup mysqld --user=mysql --init-file=/var/lib/mysql/docker-init.sql >>/var/log/mysqld.log 2>&1 & - local i=0 - while [ $i -lt 60 ]; do - mysqladmin ping --silent >/dev/null 2>&1 && return 0 - sleep 1; i=$((i+1)) - done - echo "systemctl shim: mysqld did not start within 60s" >&2; return 1 -} - -stop_mysqld() { - # Send SIGTERM directly to the mysqld process so that no root password is - # needed (mysqladmin shutdown requires auth after setup changes the password). - local pidfile=/var/run/mysqld/mysqld.pid - if [ -f "$pidfile" ]; then - local pid - pid=$(cat "$pidfile" 2>/dev/null) - [ -n "$pid" ] && kill -TERM "$pid" 2>/dev/null || true - else - pkill -TERM mysqld 2>/dev/null || true - fi - # Wait for mysqld to fully stop before returning so that the subsequent - # start_mysqld call does not find MySQL still running and skip the restart. - local i=0 - while mysqladmin ping --silent >/dev/null 2>&1 && [ $i -lt 30 ]; do - sleep 1; i=$((i+1)) - done -} - -start_httpd() { - pgrep httpd >/dev/null 2>&1 && return 0 - # The mod_ssl %post scriptlet may skip cert generation without systemd. - # Generate a self-signed cert if missing so httpd can start. - if [ ! -f /etc/pki/tls/certs/localhost.crt ]; then - openssl req -newkey rsa:2048 -nodes \ - -keyout /etc/pki/tls/private/localhost.key \ - -x509 -days 365 \ - -out /etc/pki/tls/certs/localhost.crt \ - -subj '/CN=localhost' 2>/dev/null - fi - httpd -k start 2>/dev/null -} - -stop_httpd() { - httpd -k stop 2>/dev/null || true -} - -case "$action" in - start) - case "$unit" in - mysqld|mysql) start_mysqld ;; - httpd) start_httpd ;; - sendmail) exit 0 ;; # no-op: sendmail cannot run without systemd - firewalld) exit 0 ;; # no-op: firewalld not available in Docker - *) echo "systemctl shim: unsupported unit '$unit'" >&2; exit 1 ;; - esac ;; - stop) - case "$unit" in - mysqld|mysql) stop_mysqld ;; - httpd) stop_httpd ;; - *) exit 0 ;; # non-fatal for unknown units on uninstall - esac ;; - restart) - case "$unit" in - mysqld|mysql) stop_mysqld; sleep 1; start_mysqld ;; - httpd) httpd -k restart 2>/dev/null ;; - *) echo "systemctl shim: unsupported unit '$unit'" >&2; exit 1 ;; - esac ;; - is-active) - case "$unit" in - mysqld|mysql) mysqladmin ping --silent >/dev/null 2>&1 ;; - httpd) pgrep httpd >/dev/null 2>&1 ;; - *) exit 1 ;; - esac ;; - status) - case "$unit" in - mysqld|mysql) mysqladmin ping --silent >/dev/null 2>&1 && echo "active" || exit 3 ;; - httpd) pgrep httpd >/dev/null 2>&1 && echo "active" || exit 3 ;; - *) exit 3 ;; - esac ;; - enable|disable|daemon-reload|mask|unmask|is-enabled|reset-failed) - exit 0 ;; # no-op — we don't manage boot-time units - *) - echo "systemctl shim: unknown action '$action'" >&2; exit 1 ;; -esac -EOF +# The script is kept in a separate file to avoid heredoc parsing issues across +# different Docker builder versions. +COPY systemctl-shim-centos.sh /usr/local/bin/systemctl RUN chmod +x /usr/local/bin/systemctl # firewall-cmd, setsebool, and chcon need no-op shims: diff --git a/tests/dockerfiles/Dockerfile.centos-stream-9 b/tests/dockerfiles/Dockerfile.centos-stream-9 index de3d4da..3b55d6f 100644 --- a/tests/dockerfiles/Dockerfile.centos-stream-9 +++ b/tests/dockerfiles/Dockerfile.centos-stream-9 @@ -17,128 +17,9 @@ RUN mkdir -p /etc/my.cnf.d && \ # This shim replaces /usr/bin/systemctl so the setup script's # `systemctl start/stop/restart/enable/disable/daemon-reload` calls work # by managing processes directly instead. -RUN cat > /usr/local/bin/systemctl << 'EOF' -#!/bin/bash -# Minimal systemctl shim — handles the subset used by simplerisk-setup.sh -# on CentOS/RHEL without requiring a running systemd PID 1. - -# Strip --quiet / --system / other flags; find the action and unit name. -args=() -for arg in "$@"; do - [[ "$arg" == --* ]] && continue - args+=("$arg") -done -action="${args[0]:-}" -unit="${args[1]%.service}" # strip optional .service suffix - -start_mysqld() { - mysqladmin ping --silent >/dev/null 2>&1 && return 0 # already running - mkdir -p /var/run/mysqld && chown mysql:mysql /var/run/mysqld 2>/dev/null || true - # Pre-create the error log with mysql ownership so mysqld (running as mysql user) - # can open it for writing internally in addition to the shell redirect. - touch /var/log/mysqld.log && chown mysql:mysql /var/log/mysqld.log 2>/dev/null || true - # The RPM %post scriptlet may leave /var/lib/mysql in a partial state when - # systemd is unavailable (auto.cnf + binlog.index but no ibdata1). - # Re-initialize if ibdata1 is missing. - if [ ! -f /var/lib/mysql/ibdata1 ]; then - rm -f /var/lib/mysql/auto.cnf /var/lib/mysql/binlog.index 2>/dev/null || true - # Use --initialize (not --insecure) so a temporary root password is written - # to /var/log/mysqld.log as a "Note" line for simplerisk-setup.sh to read. - mysqld --initialize --user=mysql >>/var/log/mysqld.log 2>&1 - fi - # mysqld_safe and --daemonize were removed in MySQL 8.4; run as background process. - # Use --init-file to install the validate_password component at startup: - # the community RPM does not pre-install it when systemd is absent, but - # simplerisk-setup.sh calls SET GLOBAL validate_password.policy=LOW. - # Errors in --init-file are non-fatal, so this is safe on subsequent restarts - # when the component is already installed. - printf "INSTALL COMPONENT 'file://component_validate_password';\n" > /var/lib/mysql/docker-init.sql - # Append (>>) so the initialization "Note: temp password" line is preserved. - nohup mysqld --user=mysql --init-file=/var/lib/mysql/docker-init.sql >>/var/log/mysqld.log 2>&1 & - local i=0 - while [ $i -lt 60 ]; do - mysqladmin ping --silent >/dev/null 2>&1 && return 0 - sleep 1; i=$((i+1)) - done - echo "systemctl shim: mysqld did not start within 60s" >&2; return 1 -} - -stop_mysqld() { - # Send SIGTERM directly to the mysqld process so that no root password is - # needed (mysqladmin shutdown requires auth after setup changes the password). - local pidfile=/var/run/mysqld/mysqld.pid - if [ -f "$pidfile" ]; then - local pid - pid=$(cat "$pidfile" 2>/dev/null) - [ -n "$pid" ] && kill -TERM "$pid" 2>/dev/null || true - else - pkill -TERM mysqld 2>/dev/null || true - fi - # Wait for mysqld to fully stop before returning so that the subsequent - # start_mysqld call does not find MySQL still running and skip the restart. - local i=0 - while mysqladmin ping --silent >/dev/null 2>&1 && [ $i -lt 30 ]; do - sleep 1; i=$((i+1)) - done -} - -start_httpd() { - pgrep httpd >/dev/null 2>&1 && return 0 - # The mod_ssl %post scriptlet may skip cert generation without systemd. - # Generate a self-signed cert if missing so httpd can start. - if [ ! -f /etc/pki/tls/certs/localhost.crt ]; then - openssl req -newkey rsa:2048 -nodes \ - -keyout /etc/pki/tls/private/localhost.key \ - -x509 -days 365 \ - -out /etc/pki/tls/certs/localhost.crt \ - -subj '/CN=localhost' 2>/dev/null - fi - httpd -k start 2>/dev/null -} - -stop_httpd() { - httpd -k stop 2>/dev/null || true -} - -case "$action" in - start) - case "$unit" in - mysqld|mysql) start_mysqld ;; - httpd) start_httpd ;; - sendmail) exit 0 ;; # no-op: sendmail cannot run without systemd - firewalld) exit 0 ;; # no-op: firewalld not available in Docker - *) echo "systemctl shim: unsupported unit '$unit'" >&2; exit 1 ;; - esac ;; - stop) - case "$unit" in - mysqld|mysql) stop_mysqld ;; - httpd) stop_httpd ;; - *) exit 0 ;; # non-fatal for unknown units on uninstall - esac ;; - restart) - case "$unit" in - mysqld|mysql) stop_mysqld; sleep 1; start_mysqld ;; - httpd) httpd -k restart 2>/dev/null ;; - *) echo "systemctl shim: unsupported unit '$unit'" >&2; exit 1 ;; - esac ;; - is-active) - case "$unit" in - mysqld|mysql) mysqladmin ping --silent >/dev/null 2>&1 ;; - httpd) pgrep httpd >/dev/null 2>&1 ;; - *) exit 1 ;; - esac ;; - status) - case "$unit" in - mysqld|mysql) mysqladmin ping --silent >/dev/null 2>&1 && echo "active" || exit 3 ;; - httpd) pgrep httpd >/dev/null 2>&1 && echo "active" || exit 3 ;; - *) exit 3 ;; - esac ;; - enable|disable|daemon-reload|mask|unmask|is-enabled|reset-failed) - exit 0 ;; # no-op — we don't manage boot-time units - *) - echo "systemctl shim: unknown action '$action'" >&2; exit 1 ;; -esac -EOF +# The script is kept in a separate file to avoid heredoc parsing issues across +# different Docker builder versions. +COPY systemctl-shim-centos.sh /usr/local/bin/systemctl RUN chmod +x /usr/local/bin/systemctl # firewall-cmd, setsebool, and chcon need no-op shims: diff --git a/tests/dockerfiles/Dockerfile.debian-12 b/tests/dockerfiles/Dockerfile.debian-12 index 19343e5..e6c086a 100644 --- a/tests/dockerfiles/Dockerfile.debian-12 +++ b/tests/dockerfiles/Dockerfile.debian-12 @@ -18,83 +18,9 @@ RUN mkdir -p /etc/mysql/conf.d && \ # MySQL 8.4 community server on Debian ships only systemd units (no init.d script). # This wrapper lets `service mysql start/stop/restart/status` work in a non-systemd # container by invoking mysqld directly via the helpers bundled with the package. -RUN mkdir -p /etc/init.d && cat > /etc/init.d/mysql << 'EOF' -#!/bin/bash -### BEGIN INIT INFO -# Provides: mysql -# Required-Start: $local_fs $network -# Required-Stop: $local_fs -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: MySQL Community Server -# Description: MySQL Community Server wrapper for non-systemd containers -### END INIT INFO - -PIDFILE=/var/run/mysqld/mysqld.pid -MYSQLD=/usr/sbin/mysqld -HELPERS=/usr/share/mysql-8.4/mysql-helpers - -do_start() { - if [ ! -x "$MYSQLD" ]; then - echo "mysqld not found at $MYSQLD" >&2 - return 1 - fi - # Source helpers for verify_ready / verify_database / get_running - . "$HELPERS" - verify_ready "" - verify_database "" - if [ "$(get_running)" = "1" ]; then - echo "MySQL is already running" - return 0 - fi - mkdir -p /var/run/mysqld && chown mysql:mysql /var/run/mysqld - su -s /bin/bash mysql -c "$MYSQLD --daemonize --pid-file=$PIDFILE" 2>&1 - # Wait for MySQL to accept connections (up to 30s) - local i=0 - while [ $i -lt 30 ]; do - mysqladmin ping --silent >/dev/null 2>&1 && return 0 - sleep 1 - i=$((i+1)) - done - echo "MySQL did not start within 30 seconds" >&2 - return 1 -} - -do_stop() { - if [ -f "$PIDFILE" ]; then - local pid - pid=$(cat "$PIDFILE" 2>/dev/null) || return 0 - kill "$pid" 2>/dev/null || true - local i=0 - while [ -d "/proc/$pid" ] && [ $i -lt 20 ]; do - sleep 1 - i=$((i+1)) - done - rm -f "$PIDFILE" - fi - return 0 -} - -do_status() { - if [ ! -x "$MYSQLD" ]; then - return 3 - fi - . "$HELPERS" - if [ "$(get_running)" = "1" ]; then - return 0 - else - return 1 - fi -} - -case "$1" in - start) do_start ;; - stop) do_stop ;; - restart|force-reload) do_stop; sleep 1; do_start ;; - status) do_status ;; - *) echo "Usage: $0 {start|stop|restart|force-reload|status}"; exit 1 ;; -esac -EOF +# The script is kept in a separate file to avoid heredoc parsing issues across +# different Docker builder versions. +COPY mysql-init-debian.sh /etc/init.d/mysql RUN chmod +x /etc/init.d/mysql CMD ["/bin/bash"] diff --git a/tests/dockerfiles/Dockerfile.debian-13 b/tests/dockerfiles/Dockerfile.debian-13 index 368bfda..d17be3a 100644 --- a/tests/dockerfiles/Dockerfile.debian-13 +++ b/tests/dockerfiles/Dockerfile.debian-13 @@ -18,83 +18,9 @@ RUN mkdir -p /etc/mysql/conf.d && \ # MySQL 8.4 community server on Debian ships only systemd units (no init.d script). # This wrapper lets `service mysql start/stop/restart/status` work in a non-systemd # container by invoking mysqld directly via the helpers bundled with the package. -RUN mkdir -p /etc/init.d && cat > /etc/init.d/mysql << 'EOF' -#!/bin/bash -### BEGIN INIT INFO -# Provides: mysql -# Required-Start: $local_fs $network -# Required-Stop: $local_fs -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: MySQL Community Server -# Description: MySQL Community Server wrapper for non-systemd containers -### END INIT INFO - -PIDFILE=/var/run/mysqld/mysqld.pid -MYSQLD=/usr/sbin/mysqld -HELPERS=/usr/share/mysql-8.4/mysql-helpers - -do_start() { - if [ ! -x "$MYSQLD" ]; then - echo "mysqld not found at $MYSQLD" >&2 - return 1 - fi - # Source helpers for verify_ready / verify_database / get_running - . "$HELPERS" - verify_ready "" - verify_database "" - if [ "$(get_running)" = "1" ]; then - echo "MySQL is already running" - return 0 - fi - mkdir -p /var/run/mysqld && chown mysql:mysql /var/run/mysqld - su -s /bin/bash mysql -c "$MYSQLD --daemonize --pid-file=$PIDFILE" 2>&1 - # Wait for MySQL to accept connections (up to 30s) - local i=0 - while [ $i -lt 30 ]; do - mysqladmin ping --silent >/dev/null 2>&1 && return 0 - sleep 1 - i=$((i+1)) - done - echo "MySQL did not start within 30 seconds" >&2 - return 1 -} - -do_stop() { - if [ -f "$PIDFILE" ]; then - local pid - pid=$(cat "$PIDFILE" 2>/dev/null) || return 0 - kill "$pid" 2>/dev/null || true - local i=0 - while [ -d "/proc/$pid" ] && [ $i -lt 20 ]; do - sleep 1 - i=$((i+1)) - done - rm -f "$PIDFILE" - fi - return 0 -} - -do_status() { - if [ ! -x "$MYSQLD" ]; then - return 3 - fi - . "$HELPERS" - if [ "$(get_running)" = "1" ]; then - return 0 - else - return 1 - fi -} - -case "$1" in - start) do_start ;; - stop) do_stop ;; - restart|force-reload) do_stop; sleep 1; do_start ;; - status) do_status ;; - *) echo "Usage: $0 {start|stop|restart|force-reload|status}"; exit 1 ;; -esac -EOF +# The script is kept in a separate file to avoid heredoc parsing issues across +# different Docker builder versions. +COPY mysql-init-debian.sh /etc/init.d/mysql RUN chmod +x /etc/init.d/mysql CMD ["/bin/bash"] diff --git a/tests/dockerfiles/mysql-init-debian.sh b/tests/dockerfiles/mysql-init-debian.sh new file mode 100644 index 0000000..eea5af5 --- /dev/null +++ b/tests/dockerfiles/mysql-init-debian.sh @@ -0,0 +1,75 @@ +#!/bin/bash +### BEGIN INIT INFO +# Provides: mysql +# Required-Start: $local_fs $network +# Required-Stop: $local_fs +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: MySQL Community Server +# Description: MySQL Community Server wrapper for non-systemd containers +### END INIT INFO + +PIDFILE=/var/run/mysqld/mysqld.pid +MYSQLD=/usr/sbin/mysqld +HELPERS=/usr/share/mysql-8.4/mysql-helpers + +do_start() { + if [ ! -x "$MYSQLD" ]; then + echo "mysqld not found at $MYSQLD" >&2 + return 1 + fi + # Source helpers for verify_ready / verify_database / get_running + . "$HELPERS" + verify_ready "" + verify_database "" + if [ "$(get_running)" = "1" ]; then + echo "MySQL is already running" + return 0 + fi + mkdir -p /var/run/mysqld && chown mysql:mysql /var/run/mysqld + su -s /bin/bash mysql -c "$MYSQLD --daemonize --pid-file=$PIDFILE" 2>&1 + # Wait for MySQL to accept connections (up to 30s) + local i=0 + while [ $i -lt 30 ]; do + mysqladmin ping --silent >/dev/null 2>&1 && return 0 + sleep 1 + i=$((i+1)) + done + echo "MySQL did not start within 30 seconds" >&2 + return 1 +} + +do_stop() { + if [ -f "$PIDFILE" ]; then + local pid + pid=$(cat "$PIDFILE" 2>/dev/null) || return 0 + kill "$pid" 2>/dev/null || true + local i=0 + while [ -d "/proc/$pid" ] && [ $i -lt 20 ]; do + sleep 1 + i=$((i+1)) + done + rm -f "$PIDFILE" + fi + return 0 +} + +do_status() { + if [ ! -x "$MYSQLD" ]; then + return 3 + fi + . "$HELPERS" + if [ "$(get_running)" = "1" ]; then + return 0 + else + return 1 + fi +} + +case "$1" in + start) do_start ;; + stop) do_stop ;; + restart|force-reload) do_stop; sleep 1; do_start ;; + status) do_status ;; + *) echo "Usage: $0 {start|stop|restart|force-reload|status}"; exit 1 ;; +esac diff --git a/tests/dockerfiles/systemctl-shim-centos.sh b/tests/dockerfiles/systemctl-shim-centos.sh new file mode 100644 index 0000000..4ca6202 --- /dev/null +++ b/tests/dockerfiles/systemctl-shim-centos.sh @@ -0,0 +1,120 @@ +#!/bin/bash +# Minimal systemctl shim — handles the subset used by simplerisk-setup.sh +# on CentOS/RHEL without requiring a running systemd PID 1. + +# Strip --quiet / --system / other flags; find the action and unit name. +args=() +for arg in "$@"; do + [[ "$arg" == --* ]] && continue + args+=("$arg") +done +action="${args[0]:-}" +unit="${args[1]%.service}" # strip optional .service suffix + +start_mysqld() { + mysqladmin ping --silent >/dev/null 2>&1 && return 0 # already running + mkdir -p /var/run/mysqld && chown mysql:mysql /var/run/mysqld 2>/dev/null || true + # Pre-create the error log with mysql ownership so mysqld (running as mysql user) + # can open it for writing internally in addition to the shell redirect. + touch /var/log/mysqld.log && chown mysql:mysql /var/log/mysqld.log 2>/dev/null || true + # The RPM %post scriptlet may leave /var/lib/mysql in a partial state when + # systemd is unavailable (auto.cnf + binlog.index but no ibdata1). + # Re-initialize if ibdata1 is missing. + if [ ! -f /var/lib/mysql/ibdata1 ]; then + rm -f /var/lib/mysql/auto.cnf /var/lib/mysql/binlog.index 2>/dev/null || true + # Use --initialize (not --insecure) so a temporary root password is written + # to /var/log/mysqld.log as a "Note" line for simplerisk-setup.sh to read. + mysqld --initialize --user=mysql >>/var/log/mysqld.log 2>&1 + fi + # mysqld_safe and --daemonize were removed in MySQL 8.4; run as background process. + # Use --init-file to install the validate_password component at startup: + # the community RPM does not pre-install it when systemd is absent, but + # simplerisk-setup.sh calls SET GLOBAL validate_password.policy=LOW. + # Errors in --init-file are non-fatal, so this is safe on subsequent restarts + # when the component is already installed. + printf "INSTALL COMPONENT 'file://component_validate_password';\n" > /var/lib/mysql/docker-init.sql + # Append (>>) so the initialization "Note: temp password" line is preserved. + nohup mysqld --user=mysql --init-file=/var/lib/mysql/docker-init.sql >>/var/log/mysqld.log 2>&1 & + local i=0 + while [ $i -lt 60 ]; do + mysqladmin ping --silent >/dev/null 2>&1 && return 0 + sleep 1; i=$((i+1)) + done + echo "systemctl shim: mysqld did not start within 60s" >&2; return 1 +} + +stop_mysqld() { + # Send SIGTERM directly to the mysqld process so that no root password is + # needed (mysqladmin shutdown requires auth after setup changes the password). + local pidfile=/var/run/mysqld/mysqld.pid + if [ -f "$pidfile" ]; then + local pid + pid=$(cat "$pidfile" 2>/dev/null) + [ -n "$pid" ] && kill -TERM "$pid" 2>/dev/null || true + else + pkill -TERM mysqld 2>/dev/null || true + fi + # Wait for mysqld to fully stop before returning so that the subsequent + # start_mysqld call does not find MySQL still running and skip the restart. + local i=0 + while mysqladmin ping --silent >/dev/null 2>&1 && [ $i -lt 30 ]; do + sleep 1; i=$((i+1)) + done +} + +start_httpd() { + pgrep httpd >/dev/null 2>&1 && return 0 + # The mod_ssl %post scriptlet may skip cert generation without systemd. + # Generate a self-signed cert if missing so httpd can start. + if [ ! -f /etc/pki/tls/certs/localhost.crt ]; then + openssl req -newkey rsa:2048 -nodes \ + -keyout /etc/pki/tls/private/localhost.key \ + -x509 -days 365 \ + -out /etc/pki/tls/certs/localhost.crt \ + -subj '/CN=localhost' 2>/dev/null + fi + httpd -k start 2>/dev/null +} + +stop_httpd() { + httpd -k stop 2>/dev/null || true +} + +case "$action" in + start) + case "$unit" in + mysqld|mysql) start_mysqld ;; + httpd) start_httpd ;; + sendmail) exit 0 ;; # no-op: sendmail cannot run without systemd + firewalld) exit 0 ;; # no-op: firewalld not available in Docker + *) echo "systemctl shim: unsupported unit '$unit'" >&2; exit 1 ;; + esac ;; + stop) + case "$unit" in + mysqld|mysql) stop_mysqld ;; + httpd) stop_httpd ;; + *) exit 0 ;; # non-fatal for unknown units on uninstall + esac ;; + restart) + case "$unit" in + mysqld|mysql) stop_mysqld; sleep 1; start_mysqld ;; + httpd) httpd -k restart 2>/dev/null ;; + *) echo "systemctl shim: unsupported unit '$unit'" >&2; exit 1 ;; + esac ;; + is-active) + case "$unit" in + mysqld|mysql) mysqladmin ping --silent >/dev/null 2>&1 ;; + httpd) pgrep httpd >/dev/null 2>&1 ;; + *) exit 1 ;; + esac ;; + status) + case "$unit" in + mysqld|mysql) mysqladmin ping --silent >/dev/null 2>&1 && echo "active" || exit 3 ;; + httpd) pgrep httpd >/dev/null 2>&1 && echo "active" || exit 3 ;; + *) exit 3 ;; + esac ;; + enable|disable|daemon-reload|mask|unmask|is-enabled|reset-failed) + exit 0 ;; # no-op — we don't manage boot-time units + *) + echo "systemctl shim: unknown action '$action'" >&2; exit 1 ;; +esac From bf5857893d3db6ae4caa33230bb7d1e384a57f6f Mon Sep 17 00:00:00 2001 From: DASimp Date: Fri, 8 May 2026 13:34:08 -0500 Subject: [PATCH 3/5] Fix sql_mode not applying on MySQL 8.4.9: use drop-in cnf + SET GLOBAL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On CentOS/RHEL with MySQL 8.4.9, appending sql_mode to /etc/my.cnf did not take effect after restart — the vendor-supplied files in /etc/my.cnf.d/ appear to override it. Two-part fix: 1. Write sql_mode to /etc/my.cnf.d/zz-simplerisk.cnf (the "zz-" prefix ensures it sorts last alphabetically among included files, so it wins regardless of what other files the MySQL RPM installs). 2. After the restart, also run SET GLOBAL sql_mode=... so the change is applied to the running instance immediately, without relying solely on config-file parsing order. Also enhances the CI diagnostics step to dump /etc/my.cnf, all files in /etc/my.cnf.d/, and the live sql_mode value on failure. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/install-test.yml | 19 ++++++++++++++----- simplerisk-setup.sh | 13 +++++++++---- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/.github/workflows/install-test.yml b/.github/workflows/install-test.yml index aeb356a..dee0070 100644 --- a/.github/workflows/install-test.yml +++ b/.github/workflows/install-test.yml @@ -111,14 +111,23 @@ jobs: if: failure() run: | CONTAINER="simplerisk-test-${{ matrix.os-slug }}" - echo "=== journalctl (last 100 lines) ===" && \ - docker exec "$CONTAINER" journalctl -n 100 2>/dev/null || true + echo "=== /etc/my.cnf ===" && \ + docker exec "$CONTAINER" cat /etc/my.cnf 2>/dev/null || true + echo "=== /etc/my.cnf.d/ ===" && \ + docker exec "$CONTAINER" ls -la /etc/my.cnf.d/ 2>/dev/null || true + echo "=== /etc/my.cnf.d/* contents ===" && \ + docker exec "$CONTAINER" sh -c 'for f in /etc/my.cnf.d/*.cnf; do echo "--- $f ---"; cat "$f"; done' 2>/dev/null || true + echo "=== MySQL sql_mode ===" && \ + docker exec "$CONTAINER" sh -c \ + 'PW=$(grep "MYSQL ROOT PASSWORD:" /root/passwords.txt 2>/dev/null | cut -d" " -f4); mysql -uroot -p"$PW" -e "SELECT @@sql_mode;" 2>/dev/null' || true + echo "=== journalctl (last 50 lines) ===" && \ + docker exec "$CONTAINER" journalctl -n 50 2>/dev/null || true echo "=== Apache error log ===" && \ docker exec "$CONTAINER" cat /var/log/apache2/error.log 2>/dev/null || \ docker exec "$CONTAINER" cat /var/log/httpd/error_log 2>/dev/null || true - echo "=== MySQL error log ===" && \ - docker exec "$CONTAINER" cat /var/log/mysql/error.log 2>/dev/null || \ - docker exec "$CONTAINER" cat /var/log/mysqld.log 2>/dev/null || true + echo "=== MySQL error log (last 30 lines) ===" && \ + docker exec "$CONTAINER" tail -30 /var/log/mysql/error.log 2>/dev/null || \ + docker exec "$CONTAINER" tail -30 /var/log/mysqld.log 2>/dev/null || true echo "=== /root/passwords.txt ===" && \ docker exec "$CONTAINER" cat /root/passwords.txt 2>/dev/null || true diff --git a/simplerisk-setup.sh b/simplerisk-setup.sh index fde4428..fe93fd0 100755 --- a/simplerisk-setup.sh +++ b/simplerisk-setup.sh @@ -627,13 +627,18 @@ EOF print_status 'Configuring MySQL...' set_up_database /var/log/mysqld.log - cat << EOF >> /etc/my.cnf -[mysqld] -sql_mode=ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION -EOF + # Write sql_mode to a dedicated drop-in file so it is read last (alphabetically + # "zz-") and is not overridden by any vendor-supplied file in /etc/my.cnf.d/. + # The same setting is applied via SET GLOBAL after the restart as a safety net + # for MySQL 8.4+ where config-file precedence changed. + printf '[mysqld]\nsql_mode=ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION\n' \ + > /etc/my.cnf.d/zz-simplerisk.cnf print_status 'Restarting MySQL to load the new configuration...' exec_cmd 'systemctl restart mysqld' + exec_cmd "mysql -uroot -p\"${NEW_MYSQL_ROOT_PASSWORD}\" \ + -e \"SET GLOBAL sql_mode='ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';\" \ + 2>/dev/null" print_status 'Removing the SimpleRisk database file...' exec_cmd 'rm -r /var/www/simplerisk/database.sql' From d042a01f282bd03ee0c413d77a6b54735cef6a5d Mon Sep 17 00:00:00 2001 From: DASimp Date: Fri, 8 May 2026 14:07:02 -0500 Subject: [PATCH 4/5] Make uninstaller resilient to partial/failed installs Switch all package removal steps in uninstall_ubuntu_debian, uninstall_centos_rhel, and uninstall_suse from exec_cmd (bail on failure) to exec_cmd_nobail so the uninstaller continues even when packages were never installed or only partially set up. Co-Authored-By: Claude Sonnet 4.6 --- simplerisk-setup.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/simplerisk-setup.sh b/simplerisk-setup.sh index fe93fd0..7b08e19 100755 --- a/simplerisk-setup.sh +++ b/simplerisk-setup.sh @@ -839,9 +839,9 @@ uninstall_ubuntu_debian(){ exec_cmd_nobail 'rm -rf /var/www/simplerisk' print_status 'Removing installed packages...' - exec_cmd "apt-get purge -y 'php*' 'libapache2-mod-php*' apache2 apache2-utils apache2-bin mysql-server mysql-client mysql-common sendmail sendmail-bin" - exec_cmd 'apt-get autoremove -y' - exec_cmd 'apt-get autoclean' + exec_cmd_nobail "apt-get purge -y 'php*' 'libapache2-mod-php*' apache2 apache2-utils apache2-bin mysql-server mysql-client mysql-common sendmail sendmail-bin" + exec_cmd_nobail 'apt-get autoremove -y' + exec_cmd_nobail 'apt-get autoclean' if [ "${OS}" = "${DEBIAN_OSVAR}" ]; then print_status 'Removing added repositories and keys...' @@ -884,8 +884,8 @@ uninstall_centos_rhel(){ exec_cmd_nobail 'mv /etc/httpd/conf.d/welcome.conf.disabled /etc/httpd/conf.d/welcome.conf 2>/dev/null || true' print_status 'Removing installed packages...' - exec_cmd "dnf -y remove httpd mod_ssl 'php*' mysql-community-server mysql-community-client sendmail sendmail-cf m4" - exec_cmd 'dnf -y autoremove' + exec_cmd_nobail "dnf -y remove httpd mod_ssl 'php*' mysql-community-server mysql-community-client sendmail sendmail-cf m4" + exec_cmd_nobail 'dnf -y autoremove' print_status 'Removing MySQL and PHP repositories...' local major_version="${VER%%.*}" @@ -928,8 +928,8 @@ uninstall_suse(){ exec_cmd_nobail "sed -i '/LoadModule rewrite_module.*mod_rewrite.so/d' /etc/apache2/loadmodule.conf" print_status 'Removing installed packages...' - exec_cmd "zypper -n remove apache2 mysql-community-server 'php8*' apache2-mod_php8" - exec_cmd 'zypper -n autoremove' + exec_cmd_nobail "zypper -n remove apache2 mysql-community-server 'php8*' apache2-mod_php8" + exec_cmd_nobail 'zypper -n autoremove' print_status 'Removing MySQL repository...' exec_cmd_nobail 'rpm -e mysql84-community-release-sl15 2>/dev/null || true' From 565c45b56bff5b950b7d41b56000f4111576447f Mon Sep 17 00:00:00 2001 From: DASimp Date: Fri, 8 May 2026 14:13:07 -0500 Subject: [PATCH 5/5] Pin MySQL install to 8.4 LTS on CentOS/RHEL, disable 9.x repo The mysql84-community-release RPM (updated in-place to -3) now also enables a mysql-9.7-lts-community repo. DNF was resolving mysql-community-server to 9.7.0 instead of 8.4.x. Add --disablerepo='mysql-9*' to the install command so we always get the 8.4 LTS release. Co-Authored-By: Claude Sonnet 4.6 --- simplerisk-setup.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/simplerisk-setup.sh b/simplerisk-setup.sh index 7b08e19..7b559b0 100755 --- a/simplerisk-setup.sh +++ b/simplerisk-setup.sh @@ -574,7 +574,9 @@ setup_centos_rhel(){ # mysql-community-server (same files). Exclude it explicitly so DNF does # not pull it in as a weak dependency. The exclude is a no-op on el9/el8 # where mysql8.4-server does not exist in any enabled repo. - exec_cmd "dnf install -y mysql-community-server --exclude 'mariadb*' --exclude 'mysql8.4*'" + # The mysql84-community-release RPM also enables a mysql-9.x-lts-community + # repo; disable it so DNF resolves mysql-community-server from 8.4, not 9.x. + exec_cmd "dnf install -y mysql-community-server --exclude 'mariadb*' --exclude 'mysql8.4*' --disablerepo='mysql-9*'" print_status 'Enabling and starting MySQL database server...' exec_cmd 'systemctl enable mysqld'