diff --git a/docs/superpowers/plans/2026-07-05-native-port-takeover.md b/docs/superpowers/plans/2026-07-05-native-port-takeover.md new file mode 100644 index 0000000..1e7faf8 --- /dev/null +++ b/docs/superpowers/plans/2026-07-05-native-port-takeover.md @@ -0,0 +1,1304 @@ +# Native MariaDB Port Takeover Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Let `install_db.sh` install `alt-mariadb` on the standard port 3306 by first +moving DirectAdmin's native CustomBuild-managed MariaDB off that port (to 3307 by +default), so any tool assuming the default MySQL port can reach `alt-mariadb` without +custom configuration. + +**Architecture:** A new, independent script, +`scripts/setup/native_mariadb_port_takeover.sh`, holds all logic for the takeover +(OS guard, disabling CustomBuild's MySQL management via `da build set mysql_inst no`, +rewriting the native server's port config, restarting native MariaDB and DirectAdmin, +verifying connectivity) with full rollback on any failure. `install_db.sh` calls it once, +early in `main()`, only when the resolved `PORT` is `3306` (which becomes the new default +when no port is given on the CLI). + +**Tech Stack:** Bash, `da` CLI (DirectAdmin's official CustomBuild wrapper), `systemctl`, +existing plugin helper library `mysql_common.sh`. + +## Global Constraints + +- Match existing test conventions in `tests/`: plain bash scripts with `fail()`/custom + asserts, fake command stubs installed via `PATH`/env-var injection, no external test + framework. +- Every new/changed script keeps `set -Eeuo pipefail` (the `-E` flag matters here: it's + what makes the `ERR` trap fire inside functions, which the rollback design depends on) + and the existing `log()`/`die()` pattern. +- Runs only on AlmaLinux 8 or 9 — a stricter, separate check from `install_db.sh`'s own + permissive `detect_os()`, refusing immediately (before touching anything) on CloudLinux, + Rocky, CentOS, plain RHEL, or AlmaLinux outside 8/9. +- Use `da build set mysql_inst no` / `da build set mysql_inst ` (DirectAdmin's + supported CLI) — never a raw edit of CustomBuild's `options.conf`. +- Any failure from the point CustomBuild management is disabled onward must roll back + everything done so far (restore backed-up config files, restore `mysql_inst`, restart + native MariaDB back on 3306, restart DirectAdmin again if it was already restarted) and + exit non-zero. `install_db.sh` must not proceed to installing `alt-mariadb` if this + script exits non-zero, for any reason. +- Applies only to a fresh `alt-mariadb` install — no already-running `alt-mariadb` + instance needs to be migrated to a new port. +- Reuse `mysql_common.sh` helpers (`mysql_read_key_value`, `mysql_detect_binary`, + `mysql_service_name`) rather than reimplementing them. + +--- + +## Task 1: Make `install_db.sh`'s PORT argument optional, defaulting to 3306 + +**Files:** +- Modify: `scripts/install_db.sh` (`usage()` and `apply_cli_args()`) +- Test: `tests/install_db_cli_test.sh` (existing, one assertion updated, new scenario added) + +**Interfaces:** +- Produces: `install_db.sh [PORT]` — when `PORT` is omitted, the global + `PORT` variable is set to `"3306"`. Consumed by Task 5's `maybe_takeover_native_port()`, + which checks `[[ "$PORT" != "3306" ]]`. + +- [ ] **Step 1: Update `usage()` in `scripts/install_db.sh`** + +Find the existing `usage()` function (currently prints `Użycie:\n $0 ` +followed by three examples and a list of allowed branches) and replace it with: + +```bash +usage() { + cat < [PORT] + +Przykłady: + $0 10.11 + $0 10.11 33033 + $0 11.4 3020 + $0 11.8 3021 + +Dozwolone gałęzie MariaDB: 10.11, 11.4, 11.8. + +Jeżeli PORT zostanie pominięty, przyjmowana jest wartość domyślna 3306. W takim +przypadku skrypt najpierw przenosi natywną instancję MariaDB zarządzaną przez +CustomBuild na inny port (patrz scripts/setup/native_mariadb_port_takeover.sh), +aby zwolnić port 3306 dla alt-mariadb. +EOF +} +``` + +- [ ] **Step 2: Update `apply_cli_args()` in `scripts/install_db.sh`** + +Find the existing `apply_cli_args()` function and replace it entirely with: + +```bash +apply_cli_args() { + if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 + fi + + if [[ $# -eq 0 ]]; then + usage + exit 2 + fi + + if [[ $# -gt 2 ]]; then + usage_error "Podano zbyt wiele argumentów. Oczekiwano: MARIADB_VERSION [PORT]." + fi + + MARIADB_VERSION="$1" + + if [[ $# -ge 2 && -n "${2:-}" ]]; then + PORT="$2" + else + PORT="3306" + fi + + MARIADB_DIR="/usr/local/mariadb-$MARIADB_VERSION" +} +``` + +This removes the old "fewer than 2 arguments is an error" branch entirely — 1 argument +(version only) is now valid, and `PORT` defaults to `"3306"`. Calling with 0 arguments +still shows usage and exits 2, unchanged. Calling with more than 2 arguments still errors, +unchanged. + +- [ ] **Step 3: Update the existing usage-text assertion in `tests/install_db_cli_test.sh`** + +Find this line: + +```bash +printf '%s\n' "$NO_ARGS_OUTPUT" | grep -Fq ' ' \ + || fail "install_db.sh usage does not show required version and port" +``` + +Replace it with: + +```bash +printf '%s\n' "$NO_ARGS_OUTPUT" | grep -Fq ' \[PORT\]' \ + || fail "install_db.sh usage does not show optional port" +``` + +- [ ] **Step 4: Add a new scenario to `tests/install_db_cli_test.sh` proving PORT defaults correctly** + +Add this immediately after the existing `NO_ARGS_OUTPUT`/`NO_ARGS_STATUS` block (before the +final `echo "install_db_cli_test: OK"` line): + +```bash +set +e +ONE_ARG_OUTPUT="$("$SCRIPT" 10.11 2>&1)" +ONE_ARG_STATUS=$? +set -e + +[ "$ONE_ARG_STATUS" -eq 1 ] \ + || fail "install_db.sh with only a version argument should reach the root check (status 1), got $ONE_ARG_STATUS: $ONE_ARG_OUTPUT" +printf '%s\n' "$ONE_ARG_OUTPUT" | grep -Fq 'Ten skrypt musi zostać uruchomiony jako root' \ + || fail "install_db.sh with only a version argument should default PORT to 3306 and reach the root check, got: $ONE_ARG_OUTPUT" +``` + +This proves argument parsing accepted a single argument (version only), defaulted `PORT` +without erroring, and proceeded all the way to the root check — without needing to run +the script as root or perform a real install. + +- [ ] **Step 5: Run the test to verify it passes** + +Run: `bash tests/install_db_cli_test.sh` +Expected: `install_db_cli_test: OK` + +- [ ] **Step 6: Commit** + +```bash +git add scripts/install_db.sh tests/install_db_cli_test.sh +git commit -m "feat: make install_db.sh's PORT argument optional, defaulting to 3306" +``` + +--- + +## Task 2: Detection functions for the native port takeover + +**Files:** +- Create: `scripts/setup/native_mariadb_port_takeover.sh` +- Test: `tests/native_mariadb_port_takeover_detect_test.sh` + +**Interfaces:** +- Produces (sourced or called by Task 3/4's own code in the same file, and directly + tested here): + - `require_root` — dies with a Polish message if not running as UID 0. + - `check_os_guard` — dies unless `/etc/os-release` (path overridable via + `OS_RELEASE_FILE`) reports `ID=almalinux` and `VERSION_ID` starting with `8` or `9`. + - `read_native_mysql_conf` — reads `NATIVE_MYSQL_CONF` (default + `/usr/local/directadmin/conf/mysql.conf`) and sets globals `NATIVE_CURRENT_USER`, + `NATIVE_CURRENT_PASS`, `NATIVE_CURRENT_PORT`, `NATIVE_CURRENT_SOCKET`, + `NATIVE_CURRENT_HOST`. Dies if `user` or `port` can't be read. + - `read_current_mysql_inst` — reads `CUSTOMBUILD_OPTIONS_CONF` (default + `/usr/local/directadmin/custombuild/options.conf`) and sets + `NATIVE_CURRENT_MYSQL_INST`. Dies if it can't be read. + - `check_native_reachable` — connects to the native engine using + `NATIVE_CURRENT_USER`/`NATIVE_CURRENT_PASS`/`NATIVE_CURRENT_PORT` over TCP to + `127.0.0.1`; dies if unreachable. + - `already_migrated` — returns 0 (true, as a shell predicate) if + `NATIVE_CURRENT_PORT` is not `"3306"`. + - `preflight_new_port_free` — dies if `NEW_NATIVE_PORT` (default `3307`) is already + listening (via `port_is_listening`). + - `port_is_listening PORT` — returns 0 if something is listening on `PORT`. +- Consumes: `mysql_read_key_value`, `mysql_detect_binary` from `mysql_common.sh`. + +- [ ] **Step 1: Write the failing test** + +Create `tests/native_mariadb_port_takeover_detect_test.sh`: + +```bash +#!/bin/bash +set -euo pipefail + +PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)" +SCRIPT="$PLUGIN_DIR/scripts/setup/native_mariadb_port_takeover.sh" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +# Source only the detection functions, without triggering main() (main() is +# added in Task 4, guarded behind a BASH_SOURCE check). +NATIVE_TAKEOVER_LOG="$TMP_DIR/takeover.log" \ + source "$SCRIPT" + +# --- check_os_guard: AlmaLinux 9 must pass --- +cat > "$TMP_DIR/os-release-alma9" <<'EOF' +ID="almalinux" +VERSION_ID="9.4" +PRETTY_NAME="AlmaLinux 9.4" +EOF +OS_RELEASE_FILE="$TMP_DIR/os-release-alma9" check_os_guard \ + || fail "AlmaLinux 9 should pass the OS guard" + +# --- check_os_guard: AlmaLinux 8 must pass --- +cat > "$TMP_DIR/os-release-alma8" <<'EOF' +ID="almalinux" +VERSION_ID="8.10" +EOF +OS_RELEASE_FILE="$TMP_DIR/os-release-alma8" check_os_guard \ + || fail "AlmaLinux 8 should pass the OS guard" + +# --- check_os_guard: CloudLinux must be refused --- +cat > "$TMP_DIR/os-release-cloudlinux" <<'EOF' +ID="cloudlinux" +VERSION_ID="8.9" +ID_LIKE="rhel fedora centos" +EOF +if OS_RELEASE_FILE="$TMP_DIR/os-release-cloudlinux" check_os_guard 2>/dev/null; then + fail "CloudLinux must be refused by the OS guard" +fi + +# --- check_os_guard: AlmaLinux 7 (out of supported range) must be refused --- +cat > "$TMP_DIR/os-release-alma7" <<'EOF' +ID="almalinux" +VERSION_ID="7.9" +EOF +if OS_RELEASE_FILE="$TMP_DIR/os-release-alma7" check_os_guard 2>/dev/null; then + fail "AlmaLinux 7 must be refused by the OS guard (only 8/9 supported)" +fi + +# --- read_native_mysql_conf + already_migrated --- +cat > "$TMP_DIR/mysql.conf.3306" <<'EOF' +user=da_admin +passwd=secret +port=3306 +socket=/var/lib/mysql/mysql.sock +host= +EOF +NATIVE_MYSQL_CONF="$TMP_DIR/mysql.conf.3306" read_native_mysql_conf \ + || fail "read_native_mysql_conf should succeed on a valid mysql.conf" +[ "$NATIVE_CURRENT_USER" = "da_admin" ] || fail "expected user da_admin, got: $NATIVE_CURRENT_USER" +[ "$NATIVE_CURRENT_PORT" = "3306" ] || fail "expected port 3306, got: $NATIVE_CURRENT_PORT" +already_migrated && fail "already_migrated should be false when native port is still 3306" + +cat > "$TMP_DIR/mysql.conf.3307" <<'EOF' +user=da_admin +passwd=secret +port=3307 +socket=/var/lib/mysql/mysql.sock +host= +EOF +NATIVE_MYSQL_CONF="$TMP_DIR/mysql.conf.3307" read_native_mysql_conf \ + || fail "read_native_mysql_conf should succeed on an already-migrated mysql.conf" +already_migrated || fail "already_migrated should be true when native port is 3307" + +# --- read_current_mysql_inst --- +cat > "$TMP_DIR/options.conf" <<'EOF' +mysql_inst=mariadb +mariadb=10.6 +EOF +CUSTOMBUILD_OPTIONS_CONF="$TMP_DIR/options.conf" read_current_mysql_inst \ + || fail "read_current_mysql_inst should succeed" +[ "$NATIVE_CURRENT_MYSQL_INST" = "mariadb" ] || fail "expected mysql_inst=mariadb, got: $NATIVE_CURRENT_MYSQL_INST" + +# --- preflight_new_port_free using a faked `ss` on PATH --- +mkdir -p "$TMP_DIR/bin" +cat > "$TMP_DIR/bin/ss" <<'STUB' +#!/bin/bash +# Pretends port 3307 is free and port 9999 is occupied. +for arg in "$@"; do + case "$arg" in + *:3307*) exit 0 ;; + *:9999*) + echo "header line" + echo "occupied line" + exit 0 + ;; + esac +done +exit 0 +STUB +chmod 755 "$TMP_DIR/bin/ss" + +PATH="$TMP_DIR/bin:$PATH" NEW_NATIVE_PORT="3307" preflight_new_port_free \ + || fail "preflight_new_port_free should pass when the target port is free" + +if PATH="$TMP_DIR/bin:$PATH" NEW_NATIVE_PORT="9999" preflight_new_port_free 2>/dev/null; then + fail "preflight_new_port_free should refuse when the target port is already occupied" +fi + +echo "native_mariadb_port_takeover_detect_test: OK" +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `bash tests/native_mariadb_port_takeover_detect_test.sh` +Expected: FAIL — `scripts/setup/native_mariadb_port_takeover.sh: No such file or directory`. + +- [ ] **Step 3: Create `scripts/setup/native_mariadb_port_takeover.sh`** + +```bash +#!/bin/bash +set -Eeuo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +COMMON_FILE="$SCRIPT_DIR/mysql_common.sh" + +NATIVE_MY_CNF="${NATIVE_MY_CNF:-/etc/my.cnf}" +NATIVE_MY_CNF_D="${NATIVE_MY_CNF_D:-/etc/my.cnf.d}" +NATIVE_MYSQL_CONF="${NATIVE_MYSQL_CONF:-/usr/local/directadmin/conf/mysql.conf}" +CUSTOMBUILD_OPTIONS_CONF="${CUSTOMBUILD_OPTIONS_CONF:-/usr/local/directadmin/custombuild/options.conf}" +NEW_NATIVE_PORT="${NEW_NATIVE_PORT:-3307}" +DA_CLI_BIN="${DA_CLI_BIN:-da}" +OS_RELEASE_FILE="${OS_RELEASE_FILE:-/etc/os-release}" + +LOG_FILE="${NATIVE_TAKEOVER_LOG:-$PLUGIN_DIR/data/logs/native-port-takeover.log}" +mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true + +log() { + printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE" + printf '[INFO] %s\n' "$*" +} + +die() { + log "ERROR: $*" + printf '[BŁĄD] %s\n' "$*" >&2 + exit 1 +} + +# --- state populated by the detection functions, consumed by later steps --- +NATIVE_CURRENT_PORT="" +NATIVE_CURRENT_SOCKET="" +NATIVE_CURRENT_HOST="" +NATIVE_CURRENT_USER="" +NATIVE_CURRENT_PASS="" +NATIVE_CURRENT_MYSQL_INST="" + +require_root() { + [ "$(id -u)" -eq 0 ] || die "Ten skrypt musi zostać uruchomiony jako root." +} + +check_os_guard() { + [ -r "$OS_RELEASE_FILE" ] || die "Nie można odczytać $OS_RELEASE_FILE." + local os_id="" os_version="" + os_id="$(awk -F= '$1=="ID"{gsub(/"/,"",$2); print $2}' "$OS_RELEASE_FILE")" + os_version="$(awk -F= '$1=="VERSION_ID"{gsub(/"/,"",$2); print $2}' "$OS_RELEASE_FILE")" + + [ "$os_id" = "almalinux" ] || die "Ten skrypt obsługuje wyłącznie AlmaLinux 8/9 (wykryto: ${os_id:-nieznany})." + case "$os_version" in + 8*|9*) ;; + *) die "Ten skrypt obsługuje wyłącznie AlmaLinux 8/9 (wykryto wersję: ${os_version:-nieznana})." ;; + esac + log "Wykryto obsługiwany system: AlmaLinux $os_version." +} + +read_native_mysql_conf() { + [ -r "$NATIVE_MYSQL_CONF" ] || die "Nie znaleziono pliku $NATIVE_MYSQL_CONF." + NATIVE_CURRENT_USER="$(mysql_read_key_value "$NATIVE_MYSQL_CONF" user || true)" + NATIVE_CURRENT_PASS="$(mysql_read_key_value "$NATIVE_MYSQL_CONF" passwd || true)" + NATIVE_CURRENT_PORT="$(mysql_read_key_value "$NATIVE_MYSQL_CONF" port || true)" + NATIVE_CURRENT_SOCKET="$(mysql_read_key_value "$NATIVE_MYSQL_CONF" socket || true)" + NATIVE_CURRENT_HOST="$(mysql_read_key_value "$NATIVE_MYSQL_CONF" host || true)" + [ -n "$NATIVE_CURRENT_USER" ] || die "Nie udało się odczytać pola user z $NATIVE_MYSQL_CONF." + [ -n "$NATIVE_CURRENT_PORT" ] || die "Nie udało się odczytać pola port z $NATIVE_MYSQL_CONF." +} + +read_current_mysql_inst() { + [ -r "$CUSTOMBUILD_OPTIONS_CONF" ] || die "Nie znaleziono pliku $CUSTOMBUILD_OPTIONS_CONF." + NATIVE_CURRENT_MYSQL_INST="$(mysql_read_key_value "$CUSTOMBUILD_OPTIONS_CONF" mysql_inst || true)" + [ -n "$NATIVE_CURRENT_MYSQL_INST" ] || die "Nie udało się odczytać mysql_inst z $CUSTOMBUILD_OPTIONS_CONF." + log "Bieżąca wartość mysql_inst: $NATIVE_CURRENT_MYSQL_INST." +} + +already_migrated() { + [ "$NATIVE_CURRENT_PORT" != "3306" ] +} + +check_native_reachable() { + local bin + bin="$(mysql_detect_binary mysql)" || die "Nie znaleziono klienta mysql/mariadb do sprawdzenia natywnej instancji." + MYSQL_PWD="$NATIVE_CURRENT_PASS" "$bin" \ + --user="$NATIVE_CURRENT_USER" \ + --protocol=TCP --host=127.0.0.1 --port="$NATIVE_CURRENT_PORT" \ + -e "SELECT 1" >/dev/null 2>>"$LOG_FILE" \ + || die "Natywna instancja MariaDB nie odpowiada na porcie $NATIVE_CURRENT_PORT z bieżącymi poświadczeniami." + log "Natywna instancja MariaDB odpowiada na porcie $NATIVE_CURRENT_PORT." +} + +port_is_listening() { + local port="$1" + if command -v ss >/dev/null 2>&1; then + ss -ltn "( sport = :$port )" 2>/dev/null | awk 'NR > 1 { found=1 } END { exit !found }' + return $? + fi + if command -v lsof >/dev/null 2>&1; then + lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1 + return $? + fi + log "Nie znaleziono ss ani lsof - zakładam, że port $port jest wolny." + return 1 +} + +preflight_new_port_free() { + if port_is_listening "$NEW_NATIVE_PORT"; then + die "Port $NEW_NATIVE_PORT jest już zajęty. Zmień NEW_NATIVE_PORT i spróbuj ponownie." + fi + log "Port docelowy $NEW_NATIVE_PORT jest wolny." +} + +[ -r "$COMMON_FILE" ] || die "missing helper: $COMMON_FILE" +# shellcheck source=./mysql_common.sh +source "$COMMON_FILE" + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + : # main() is added in Task 4. +fi +``` + +Note: the test sources this file directly (`source "$SCRIPT"`). `mysql_common.sh` is +sourced unconditionally (not inside the `BASH_SOURCE[0]` guard) because both direct +execution and sourcing-for-tests need its helper functions — the test exercises +`read_native_mysql_conf`/`check_native_reachable`, which call +`mysql_read_key_value`/`mysql_detect_binary` from `mysql_common.sh`. Only the future +`main()` call (added in Task 4) needs to stay behind the guard, so sourcing this file for +tests never triggers it. + +- [ ] **Step 4: Run test to verify it passes** + +Run: `bash tests/native_mariadb_port_takeover_detect_test.sh` +Expected: `native_mariadb_port_takeover_detect_test: OK` + +- [ ] **Step 5: Make the script executable and commit** + +```bash +chmod +x scripts/setup/native_mariadb_port_takeover.sh +git add scripts/setup/native_mariadb_port_takeover.sh tests/native_mariadb_port_takeover_detect_test.sh +git commit -m "feat: add detection functions for native MariaDB port takeover" +``` + +--- + +## Task 3: Config mutation and rollback + +**Files:** +- Modify: `scripts/setup/native_mariadb_port_takeover.sh` (add mutation + rollback functions) +- Test: `tests/native_mariadb_port_takeover_mutation_test.sh` + +**Interfaces:** +- Consumes from Task 2: `NATIVE_CURRENT_MYSQL_INST`, `NATIVE_MY_CNF`, `NATIVE_MY_CNF_D`, + `NATIVE_MYSQL_CONF`, `NEW_NATIVE_PORT`, `DA_CLI_BIN`, `log`, `die`. +- Produces (used by Task 4's `main()`): + - `disable_custombuild_mysql_management` — runs `da build set mysql_inst no` (skips if + `NATIVE_CURRENT_MYSQL_INST` is already `no`), sets global `DID_SET_MYSQL_INST_NO=1`. + - `restore_custombuild_mysql_management` — no-op unless `DID_SET_MYSQL_INST_NO=1`; runs + `da build set mysql_inst "$NATIVE_CURRENT_MYSQL_INST"`. + - `apply_config_mutations` — backs up and rewrites the native port in every relevant + config file (see below) to `NEW_NATIVE_PORT`, and updates `NATIVE_MYSQL_CONF`'s + `port=` field to `NEW_NATIVE_PORT`. Populates the global array `CONFIG_BACKUPS` with + `"original_path:backup_path"` entries for every file it touched. + - `rollback_config_changes` — restores every file in `CONFIG_BACKUPS` from its backup. + +- [ ] **Step 1: Write the failing test** + +Create `tests/native_mariadb_port_takeover_mutation_test.sh`: + +```bash +#!/bin/bash +set -euo pipefail + +PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)" +SCRIPT="$PLUGIN_DIR/scripts/setup/native_mariadb_port_takeover.sh" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +mkdir -p "$TMP_DIR/bin" "$TMP_DIR/my.cnf.d" +CALL_LOG="$TMP_DIR/calls.log" + +cat > "$TMP_DIR/bin/da" <> "$CALL_LOG" +exit 0 +EOF +chmod 755 "$TMP_DIR/bin/da" + +cat > "$TMP_DIR/my.cnf" <<'EOF' +[client] +port=3306 + +[mysqld] +datadir=/var/lib/mysql +port=3306 +socket=/var/lib/mysql/mysql.sock +EOF + +cat > "$TMP_DIR/mysql.conf" <<'EOF' +user=da_admin +passwd=secret +port=3306 +socket=/var/lib/mysql/mysql.sock +host= +EOF + +NATIVE_TAKEOVER_LOG="$TMP_DIR/takeover.log" \ + source "$SCRIPT" + +NATIVE_MY_CNF="$TMP_DIR/my.cnf" +NATIVE_MY_CNF_D="$TMP_DIR/my.cnf.d" +NATIVE_MYSQL_CONF="$TMP_DIR/mysql.conf" +NEW_NATIVE_PORT="3307" +DA_CLI_BIN="$TMP_DIR/bin/da" +NATIVE_CURRENT_MYSQL_INST="mariadb" + +# --- disable_custombuild_mysql_management --- +: > "$CALL_LOG" +disable_custombuild_mysql_management +grep -Fxq "build set mysql_inst no" "$CALL_LOG" \ + || fail "expected 'da build set mysql_inst no' to be called, got: $(cat "$CALL_LOG")" +[ "$DID_SET_MYSQL_INST_NO" -eq 1 ] || fail "expected DID_SET_MYSQL_INST_NO to be set" + +# --- idempotency: already "no" must skip calling da --- +: > "$CALL_LOG" +DID_SET_MYSQL_INST_NO=0 +NATIVE_CURRENT_MYSQL_INST="no" +disable_custombuild_mysql_management +[ ! -s "$CALL_LOG" ] || fail "expected no 'da' call when mysql_inst is already 'no'" +[ "$DID_SET_MYSQL_INST_NO" -eq 0 ] || fail "DID_SET_MYSQL_INST_NO should stay 0 when already 'no'" +NATIVE_CURRENT_MYSQL_INST="mariadb" + +# --- apply_config_mutations --- +CONFIG_BACKUPS=() +apply_config_mutations + +grep -Fxq "port=3307" "$TMP_DIR/mysql.conf" \ + || fail "expected mysql.conf port to be rewritten to 3307, got: $(cat "$TMP_DIR/mysql.conf")" +grep -Fxq "passwd=secret" "$TMP_DIR/mysql.conf" \ + || fail "expected mysql.conf passwd to be preserved untouched" + +grep -A2 '^\[mysqld\]' "$TMP_DIR/my.cnf" | grep -Fxq "port=3307" \ + || fail "expected [mysqld] port in my.cnf to be rewritten to 3307" +grep -A1 '^\[client\]' "$TMP_DIR/my.cnf" | grep -Fxq "port=3306" \ + || fail "expected [client] port in my.cnf to be left at 3306 (only server sections rewritten)" + +[ "${#CONFIG_BACKUPS[@]}" -ge 2 ] \ + || fail "expected at least 2 backup entries (my.cnf + mysql.conf), got ${#CONFIG_BACKUPS[@]}" + +# --- rollback_config_changes --- +rollback_config_changes + +grep -A2 '^\[mysqld\]' "$TMP_DIR/my.cnf" | grep -Fxq "port=3306" \ + || fail "expected [mysqld] port in my.cnf to be restored to 3306 after rollback" +grep -Fxq "port=3306" "$TMP_DIR/mysql.conf" \ + || fail "expected mysql.conf port to be restored to 3306 after rollback" + +# --- restore_custombuild_mysql_management --- +: > "$CALL_LOG" +DID_SET_MYSQL_INST_NO=1 +restore_custombuild_mysql_management +grep -Fxq "build set mysql_inst mariadb" "$CALL_LOG" \ + || fail "expected 'da build set mysql_inst mariadb' to be called on rollback, got: $(cat "$CALL_LOG")" + +echo "native_mariadb_port_takeover_mutation_test: OK" +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `bash tests/native_mariadb_port_takeover_mutation_test.sh` +Expected: FAIL — `disable_custombuild_mysql_management: command not found` (the function +doesn't exist yet). + +- [ ] **Step 3: Add mutation and rollback functions** + +Add these to `scripts/setup/native_mariadb_port_takeover.sh`, after `preflight_new_port_free` +and before the trailing `if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then ... fi` block: + +```bash +DID_SET_MYSQL_INST_NO=0 +CONFIG_BACKUPS=() + +disable_custombuild_mysql_management() { + if [ "$NATIVE_CURRENT_MYSQL_INST" = "no" ]; then + log "mysql_inst jest już ustawione na 'no' - pomijam." + return 0 + fi + "$DA_CLI_BIN" build set mysql_inst no >>"$LOG_FILE" 2>&1 \ + || die "Nie udało się ustawić mysql_inst=no przez '$DA_CLI_BIN build set mysql_inst no'." + DID_SET_MYSQL_INST_NO=1 + log "Ustawiono mysql_inst=no przez CustomBuild." +} + +restore_custombuild_mysql_management() { + [ "$DID_SET_MYSQL_INST_NO" -eq 1 ] || return 0 + "$DA_CLI_BIN" build set mysql_inst "$NATIVE_CURRENT_MYSQL_INST" >>"$LOG_FILE" 2>&1 \ + || log "OSTRZEŻENIE: nie udało się przywrócić mysql_inst=$NATIVE_CURRENT_MYSQL_INST." + log "Przywrócono mysql_inst=$NATIVE_CURRENT_MYSQL_INST." +} + +backup_config_file() { + local file="$1" + local backup + backup="${file}.bak.$(date +%Y%m%d%H%M%S)" + cp -a "$file" "$backup" + CONFIG_BACKUPS+=("$file:$backup") + log "Utworzono kopię zapasową $file jako $backup." +} + +rewrite_port_in_file() { + local file="$1" new_port="$2" + local tmp changed=0 + + [ -r "$file" ] || return 1 + tmp="$(mktemp)" + + awk -v new_port="$new_port" ' + BEGIN { in_section = 0; changed = 0 } + /^[[:space:]]*\[[^]]+\][[:space:]]*$/ { + section = tolower($0) + gsub(/^[[:space:]]*\[[[:space:]]*/, "", section) + gsub(/[[:space:]]*\][[:space:]]*$/, "", section) + in_section = (section == "mysqld" || section == "mariadb" || section == "server") + print + next + } + in_section && /^[[:space:]]*port[[:space:]]*=/ { + print "port=" new_port + changed = 1 + next + } + { print } + END { if (changed) exit 0; else exit 1 } + ' "$file" > "$tmp" + local awk_status=$? + + if [ "$awk_status" -eq 0 ]; then + cp "$tmp" "$file" + changed=1 + fi + rm -f "$tmp" + [ "$changed" -eq 1 ] +} + +find_and_rewrite_native_port_configs() { + local any_changed=0 + local f + + for f in "$NATIVE_MY_CNF" "$NATIVE_MY_CNF_D"/*.cnf; do + [ -f "$f" ] || continue + if grep -Eq '^[[:space:]]*port[[:space:]]*=[[:space:]]*3306[[:space:]]*$' "$f"; then + backup_config_file "$f" + if rewrite_port_in_file "$f" "$NEW_NATIVE_PORT"; then + any_changed=1 + log "Zaktualizowano port w $f." + fi + fi + done + + if [ "$any_changed" -eq 0 ]; then + backup_config_file "$NATIVE_MY_CNF" + if grep -Eq '^[[:space:]]*\[mysqld\][[:space:]]*$' "$NATIVE_MY_CNF"; then + awk -v new_port="$NEW_NATIVE_PORT" ' + { print } + /^[[:space:]]*\[mysqld\][[:space:]]*$/ && !done { print "port=" new_port; done=1 } + ' "$NATIVE_MY_CNF" > "${NATIVE_MY_CNF}.tmp" && mv "${NATIVE_MY_CNF}.tmp" "$NATIVE_MY_CNF" + else + printf '\n[mysqld]\nport=%s\n' "$NEW_NATIVE_PORT" >> "$NATIVE_MY_CNF" + fi + log "Brak istniejącej dyrektywy port= w sekcji serwera - dodano port=$NEW_NATIVE_PORT do $NATIVE_MY_CNF." + fi +} + +update_native_mysql_conf_port() { + local target_port="$1" + local tmp + tmp="$(mktemp)" + awk -v new_port="$target_port" ' + BEGIN { done = 0 } + /^[[:space:]]*port[[:space:]]*=/ { print "port=" new_port; done=1; next } + { print } + END { if (!done) print "port=" new_port } + ' "$NATIVE_MYSQL_CONF" > "$tmp" + cp "$tmp" "$NATIVE_MYSQL_CONF" + rm -f "$tmp" +} + +apply_config_mutations() { + backup_config_file "$NATIVE_MYSQL_CONF" + find_and_rewrite_native_port_configs + update_native_mysql_conf_port "$NEW_NATIVE_PORT" + log "Zaktualizowano $NATIVE_MYSQL_CONF na port $NEW_NATIVE_PORT." +} + +rollback_config_changes() { + local pair file backup + for pair in "${CONFIG_BACKUPS[@]}"; do + file="${pair%%:*}" + backup="${pair#*:}" + if [ -r "$backup" ]; then + cp -a "$backup" "$file" + log "Przywrócono $file z kopii zapasowej $backup." + fi + done +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `bash tests/native_mariadb_port_takeover_mutation_test.sh` +Expected: `native_mariadb_port_takeover_mutation_test: OK` + +- [ ] **Step 5: Re-run Task 2's test to confirm it still passes unchanged** + +Run: `bash tests/native_mariadb_port_takeover_detect_test.sh` +Expected: `native_mariadb_port_takeover_detect_test: OK` + +- [ ] **Step 6: Commit** + +```bash +git add scripts/setup/native_mariadb_port_takeover.sh tests/native_mariadb_port_takeover_mutation_test.sh +git commit -m "feat: add config mutation and rollback for native MariaDB port takeover" +``` + +--- + +## Task 4: Service restarts, connectivity verification, and full orchestration + +**Files:** +- Modify: `scripts/setup/native_mariadb_port_takeover.sh` (add service-restart functions, + `main()`, and the `ERR`-trap-based rollback wiring) +- Test: `tests/native_mariadb_port_takeover_e2e_test.sh` + +**Interfaces:** +- Consumes from Task 2/3: everything defined so far. +- Produces: a fully working, directly executable + `scripts/setup/native_mariadb_port_takeover.sh` with these additional functions: + - `detect_native_service_name` — sets global `NATIVE_SERVICE_NAME` via + `mysql_service_name`. + - `restart_native_service` — restarts `NATIVE_SERVICE_NAME`, sets `DID_RESTART_NATIVE=1`. + - `verify_native_listening_new_port` — polls (up to 30x, 1s apart) until + `NEW_NATIVE_PORT` is listening and 3306 is not. + - `restart_directadmin_service` — restarts the `directadmin` service, sets + `DID_RESTART_DA=1`. + - `verify_da_connectivity_new_port` — confirms `NATIVE_CURRENT_USER`/`NATIVE_CURRENT_PASS` + connect over TCP to `127.0.0.1:$NEW_NATIVE_PORT`. + - `rollback_service_state` — full rollback: config files, `mysql_inst`, and (if + reached) restarting native MariaDB and DirectAdmin back to their pre-takeover state. + Wired as an `ERR` trap starting right before `disable_custombuild_mysql_management` is + called, so any failure from that point on triggers it automatically. + - `main` — orchestrates everything in order; exits 0 immediately (no mutation) if + `already_migrated` is true. + +- [ ] **Step 1: Write the failing end-to-end test** + +Create `tests/native_mariadb_port_takeover_e2e_test.sh`: + +```bash +#!/bin/bash +set -euo pipefail + +PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)" +SCRIPT="$PLUGIN_DIR/scripts/setup/native_mariadb_port_takeover.sh" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +setup_fixture() { + local dir="$1" + mkdir -p "$dir/bin" "$dir/my.cnf.d" + + cat > "$dir/my.cnf" <<'EOF' +[client] +port=3306 + +[mysqld] +datadir=/var/lib/mysql +port=3306 +socket=/var/lib/mysql/mysql.sock +EOF + + cat > "$dir/mysql.conf" <<'EOF' +user=da_admin +passwd=secret +port=3306 +socket=/var/lib/mysql/mysql.sock +host= +EOF + + cat > "$dir/options.conf" <<'EOF' +mysql_inst=mariadb +mariadb=10.6 +EOF + + cat > "$dir/os-release" <<'EOF' +ID="almalinux" +VERSION_ID="9.4" +EOF +} + +# run_takeover DIR +# Runs the script under test against the fixture already prepared at DIR +# (by a prior call to setup_fixture, possibly customized further by the +# scenario since) - writes stdout+stderr to "$DIR/run.out". Does NOT touch +# the fixture itself, so scenario-specific customizations made after +# setup_fixture (e.g. editing mysql.conf, swapping os-release) are preserved. +run_takeover() { + local dir="$1" + + env \ + NATIVE_TAKEOVER_LOG="$dir/takeover.log" \ + NATIVE_MY_CNF="$dir/my.cnf" \ + NATIVE_MY_CNF_D="$dir/my.cnf.d" \ + NATIVE_MYSQL_CONF="$dir/mysql.conf" \ + CUSTOMBUILD_OPTIONS_CONF="$dir/options.conf" \ + OS_RELEASE_FILE="$dir/os-release" \ + NEW_NATIVE_PORT="3307" \ + DA_CLI_BIN="$dir/bin/da" \ + PATH="$dir/bin:$PATH" \ + bash "$SCRIPT" > "$dir/run.out" 2>&1 +} + +# --- Fake command stubs shared by scenarios; each scenario builds its own copies --- +write_common_stubs() { + local dir="$1" + local mode="${2:-happy}" + + cat > "$dir/bin/da" <> "$dir/da_calls.log" +exit 0 +EOF + chmod 755 "$dir/bin/da" + + cat > "$dir/bin/mysql" </dev/null || echo 3306)" ]; then + exit 0 +fi +exit 1 +EOF + chmod 755 "$dir/bin/mysql" + echo "3306" > "$dir/current_port" + + cat > "$dir/bin/systemctl" <.service' and picks the first +# that succeeds - only mariadb.service "exists" here, matching a real +# AlmaLinux+MariaDB box where mysqld.service does not exist. +if [ "\$1" = "list-unit-files" ]; then + case "\$2" in + mariadb.service) exit 0 ;; + *) exit 1 ;; + esac +fi + +echo "systemctl \$*" >> "$dir/systemctl_calls.log" +if [ "\$1" = "restart" ] && [ "\$2" = "mariadb" ] && [ "$mode" = "restart-fail" ]; then + exit 1 +fi +if [ "\$1" = "restart" ] && [ "\$2" = "mariadb" ]; then + echo "3307" > "$dir/current_port" +fi +if [ "\$1" = "restart" ] && [ "\$2" = "directadmin" ] && [ "$mode" = "da-restart-fail" ]; then + exit 1 +fi +exit 0 +EOF + chmod 755 "$dir/bin/systemctl" + + cat > "$dir/bin/ss" </dev/null || echo 3306)" +for arg in "\$@"; do + case "\$arg" in + *":\$current"*) echo header; echo occupied; exit 0 ;; + esac +done +exit 0 +EOF + chmod 755 "$dir/bin/ss" +} + +# --- Scenario 1: happy path --- +DIR="$TMP_DIR/happy" +setup_fixture "$DIR" +write_common_stubs "$DIR" happy +run_takeover "$DIR" +STATUS=$? +[ "$STATUS" -eq 0 ] || fail "happy path should exit 0, got $STATUS: $(cat "$DIR/run.out")" +grep -Fxq "port=3307" "$DIR/mysql.conf" || fail "happy path should leave mysql.conf on port 3307" +grep -Fq "build set mysql_inst no" "$DIR/da_calls.log" || fail "happy path should call da build set mysql_inst no" +grep -Fq "restart mariadb" "$DIR/systemctl_calls.log" || fail "happy path should restart mariadb" +grep -Fq "restart directadmin" "$DIR/systemctl_calls.log" || fail "happy path should restart directadmin" + +# --- Scenario 2: already migrated (idempotent no-op) --- +DIR="$TMP_DIR/already" +setup_fixture "$DIR" +write_common_stubs "$DIR" happy +sed -i.orig 's/^port=3306$/port=3307/' "$DIR/mysql.conf" +run_takeover "$DIR" +STATUS=$? +[ "$STATUS" -eq 0 ] || fail "already-migrated case should exit 0, got $STATUS" +grep -Fq "build set" "$DIR/da_calls.log" 2>/dev/null && fail "already-migrated case should not touch CustomBuild settings" + +# --- Scenario 3: OS guard rejection --- +DIR="$TMP_DIR/badose" +setup_fixture "$DIR" +write_common_stubs "$DIR" happy +cat > "$DIR/os-release" <<'EOF' +ID="cloudlinux" +VERSION_ID="8.9" +EOF +set +e +run_takeover "$DIR" +STATUS=$? +set -e +[ "$STATUS" -ne 0 ] || fail "CloudLinux should be refused, not exit 0" +grep -Fq "build set" "$DIR/da_calls.log" 2>/dev/null && fail "OS-guard rejection must not touch CustomBuild settings" + +# --- Scenario 4: rollback when native restart fails --- +DIR="$TMP_DIR/restartfail" +setup_fixture "$DIR" +write_common_stubs "$DIR" restart-fail +set +e +run_takeover "$DIR" +STATUS=$? +set -e +[ "$STATUS" -ne 0 ] || fail "a failed native restart should not exit 0" +grep -Fxq "port=3306" "$DIR/mysql.conf" \ + || fail "mysql.conf should be rolled back to port 3306 after a failed native restart" +grep -Fq "build set mysql_inst mariadb" "$DIR/da_calls.log" \ + || fail "mysql_inst should be restored to mariadb after a failed native restart" + +# --- Scenario 5: rollback when DirectAdmin restart fails --- +DIR="$TMP_DIR/darestartfail" +setup_fixture "$DIR" +write_common_stubs "$DIR" da-restart-fail +set +e +run_takeover "$DIR" +STATUS=$? +set -e +[ "$STATUS" -ne 0 ] || fail "a failed directadmin restart should not exit 0" +grep -Fxq "port=3306" "$DIR/mysql.conf" \ + || fail "mysql.conf should be rolled back to port 3306 after a failed directadmin restart" +grep -Fq "build set mysql_inst mariadb" "$DIR/da_calls.log" \ + || fail "mysql_inst should be restored to mariadb after a failed directadmin restart" + +echo "native_mariadb_port_takeover_e2e_test: OK" +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `bash tests/native_mariadb_port_takeover_e2e_test.sh` +Expected: FAIL — no `main()` function exists yet, so nothing happens when the script is +executed directly. + +- [ ] **Step 3: Add service-restart, verification, and orchestration functions** + +Add these to `scripts/setup/native_mariadb_port_takeover.sh`, after `rollback_config_changes` +and before the trailing `if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then ... fi` block: + +```bash +NATIVE_SERVICE_NAME="" +DID_RESTART_NATIVE=0 +DID_RESTART_DA=0 + +detect_native_service_name() { + NATIVE_SERVICE_NAME="$(mysql_service_name)" + log "Wykryta usługa natywnej MariaDB: $NATIVE_SERVICE_NAME." +} + +restart_native_service() { + systemctl restart "$NATIVE_SERVICE_NAME" || die "Nie udało się zrestartować usługi $NATIVE_SERVICE_NAME." + DID_RESTART_NATIVE=1 +} + +verify_native_listening_new_port() { + local i + for i in $(seq 1 30); do + if port_is_listening "$NEW_NATIVE_PORT" && ! port_is_listening 3306; then + log "Natywna instancja nasłuchuje na porcie $NEW_NATIVE_PORT, port 3306 wolny." + return 0 + fi + sleep 1 + done + die "Natywna instancja nie nasłuchuje na porcie $NEW_NATIVE_PORT po restarcie (lub port 3306 wciąż zajęty)." +} + +restart_directadmin_service() { + systemctl restart directadmin || die "Nie udało się zrestartować usługi directadmin." + DID_RESTART_DA=1 +} + +verify_da_connectivity_new_port() { + local bin + bin="$(mysql_detect_binary mysql)" || die "Nie znaleziono klienta mysql/mariadb do weryfikacji łączności DA." + MYSQL_PWD="$NATIVE_CURRENT_PASS" "$bin" \ + --user="$NATIVE_CURRENT_USER" \ + --protocol=TCP --host=127.0.0.1 --port="$NEW_NATIVE_PORT" \ + -e "SELECT 1" >/dev/null 2>>"$LOG_FILE" \ + || die "DirectAdmin nie może połączyć się z natywną instancją na nowym porcie $NEW_NATIVE_PORT." + log "Potwierdzono łączność DirectAdmina z natywną instancją na porcie $NEW_NATIVE_PORT." +} + +rollback_service_state() { + rollback_config_changes + restore_custombuild_mysql_management + if [ -n "$NATIVE_SERVICE_NAME" ]; then + systemctl restart "$NATIVE_SERVICE_NAME" \ + || log "OSTRZEŻENIE: nie udało się zrestartować $NATIVE_SERVICE_NAME podczas wycofywania zmian." + fi + if [ "$DID_RESTART_DA" -eq 1 ]; then + systemctl restart directadmin \ + || log "OSTRZEŻENIE: nie udało się zrestartować directadmin podczas wycofywania zmian." + fi + log "Wycofano zmiany przejęcia portu natywnej instancji MariaDB." +} + +main() { + require_root + check_os_guard + read_native_mysql_conf + read_current_mysql_inst + + if already_migrated; then + log "Natywna instancja już działa na porcie $NATIVE_CURRENT_PORT (nie 3306) - pomijam przejęcie." + exit 0 + fi + + check_native_reachable + preflight_new_port_free + detect_native_service_name + + trap 'rollback_service_state' ERR + + disable_custombuild_mysql_management + apply_config_mutations + restart_native_service + verify_native_listening_new_port + restart_directadmin_service + verify_da_connectivity_new_port + + trap - ERR + log "Przejęcie portu natywnej instancji MariaDB zakończone powodzeniem: $NATIVE_CURRENT_PORT -> $NEW_NATIVE_PORT." +} +``` + +Then replace the trailing block at the very end of the file with: + +```bash +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi +``` + +(`mysql_common.sh` is already sourced unconditionally earlier in the file per Task 2's +Step 3 note, so it does not need to be sourced again here.) + +- [ ] **Step 4: Run test to verify it passes** + +Run: `bash tests/native_mariadb_port_takeover_e2e_test.sh` +Expected: `native_mariadb_port_takeover_e2e_test: OK` + +- [ ] **Step 5: Re-run Task 2 and Task 3's tests to confirm no regression** + +Run: +```bash +bash tests/native_mariadb_port_takeover_detect_test.sh +bash tests/native_mariadb_port_takeover_mutation_test.sh +``` +Expected: both print their own `OK` line. + +- [ ] **Step 6: Commit** + +```bash +git add scripts/setup/native_mariadb_port_takeover.sh tests/native_mariadb_port_takeover_e2e_test.sh +git commit -m "feat: add service restart, verification, and full rollback orchestration to native port takeover" +``` + +--- + +## Task 5: Wire the takeover script into `install_db.sh` + +**Files:** +- Modify: `scripts/install_db.sh` (new `NATIVE_TAKEOVER_SCRIPT` variable, new + `maybe_takeover_native_port()` function, one new call in `main()`) +- Test: `tests/install_db_native_takeover_wiring_test.sh` + +**Interfaces:** +- Consumes from Task 1: the `PORT` variable, already resolved to `"3306"` by + `apply_cli_args` when no port was given (or explicitly `"3306"`). +- Consumes from Task 4: the finished, executable + `scripts/setup/native_mariadb_port_takeover.sh` (used as the real default path; the + wiring test overrides it with a fake stub). +- Produces: `install_db.sh` refuses to proceed with the rest of its install flow if the + takeover script exits non-zero. + +- [ ] **Step 1: Write the failing test** + +Create `tests/install_db_native_takeover_wiring_test.sh`: + +```bash +#!/bin/bash +set -euo pipefail + +PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)" +REAL_SCRIPT="$PLUGIN_DIR/scripts/install_db.sh" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +# Extract the exact, real maybe_takeover_native_port() function body from the +# real script (rather than reimplementing it), so this test exercises the +# actual production logic without triggering the script's own main() (which +# needs a full install environment). +FUNCS_FILE="$TMP_DIR/funcs.sh" +{ + echo '#!/bin/bash' + echo 'log() { echo "[INFO] $*"; }' + echo 'die() { echo "[BŁĄD] $*" >&2; exit 1; }' + awk '/^maybe_takeover_native_port\(\)/,/^\}/' "$REAL_SCRIPT" +} > "$FUNCS_FILE" + +grep -q '^maybe_takeover_native_port()' "$FUNCS_FILE" \ + || fail "could not extract maybe_takeover_native_port() from $REAL_SCRIPT" + +# shellcheck source=/dev/null +source "$FUNCS_FILE" + +mkdir -p "$TMP_DIR/setup" +CALL_LOG="$TMP_DIR/calls.log" +cat > "$TMP_DIR/setup/native_mariadb_port_takeover.sh" <> "$CALL_LOG" +exit 0 +EOF +chmod 755 "$TMP_DIR/setup/native_mariadb_port_takeover.sh" + +NATIVE_TAKEOVER_SCRIPT="$TMP_DIR/setup/native_mariadb_port_takeover.sh" + +# --- Scenario 1: PORT=3306 must call the takeover script --- +PORT="3306" +: > "$CALL_LOG" +maybe_takeover_native_port +grep -Fxq "CALLED" "$CALL_LOG" || fail "expected takeover script to be called when PORT=3306" + +# --- Scenario 2: PORT!=3306 must NOT call the takeover script --- +PORT="33033" +: > "$CALL_LOG" +maybe_takeover_native_port +[ ! -s "$CALL_LOG" ] || fail "expected takeover script NOT to be called when PORT=33033" + +# --- Scenario 3: takeover script failure must abort (non-zero exit) --- +cat > "$TMP_DIR/setup/native_mariadb_port_takeover.sh" <> "$CALL_LOG" +exit 1 +EOF +chmod 755 "$TMP_DIR/setup/native_mariadb_port_takeover.sh" + +PORT="3306" +set +e +FAIL_OUTPUT="$(maybe_takeover_native_port 2>&1)" +FAIL_STATUS=$? +set -e +[ "$FAIL_STATUS" -ne 0 ] || fail "maybe_takeover_native_port should fail when the takeover script fails" +printf '%s\n' "$FAIL_OUTPUT" | grep -Fq "Przejęcie portu" \ + || fail "expected a clear error message on takeover failure, got: $FAIL_OUTPUT" + +echo "install_db_native_takeover_wiring_test: OK" +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `bash tests/install_db_native_takeover_wiring_test.sh` +Expected: FAIL — `could not extract maybe_takeover_native_port() from .../install_db.sh` +(the function doesn't exist yet). + +- [ ] **Step 3: Add `NATIVE_TAKEOVER_SCRIPT` and `maybe_takeover_native_port()` to `install_db.sh`** + +Add this near the top of `scripts/install_db.sh`, alongside the other path-configuration +variables (after the `INSTANCE_CNF="/etc/alt-mariadb.cnf"` line is a reasonable spot): + +```bash +NATIVE_TAKEOVER_SCRIPT="${NATIVE_TAKEOVER_SCRIPT:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/setup/native_mariadb_port_takeover.sh}" +``` + +Then add this new function, near `require_root` (order among functions doesn't matter in +bash as long as it's defined before `main` calls it): + +```bash +maybe_takeover_native_port() { + if [[ "$PORT" != "3306" ]]; then + return 0 + fi + + [[ -x "$NATIVE_TAKEOVER_SCRIPT" ]] || die "Nie znaleziono skryptu przejęcia portu natywnego MariaDB: $NATIVE_TAKEOVER_SCRIPT" + + log "PORT=3306 wybrany dla alt-mariadb - uruchamiam przejęcie portu natywnej instancji MariaDB." + "$NATIVE_TAKEOVER_SCRIPT" || die "Przejęcie portu natywnej instancji MariaDB nie powiodło się. Przerwano instalację alt-mariadb." +} +``` + +Finally, add a call to it in `main()`, right after `detect_os` and before `load_da_credentials`: + +```bash +main() { + apply_cli_args "$@" + validate_version + validate_port + require_root + detect_os + maybe_takeover_native_port + load_da_credentials + validate_paths + install_dependencies + resolve_download_url + download_tarball + extract_tarball + prepare_user_and_dirs + write_instance_config + write_da_style_credential_files + write_alt_metadata_env + initialize_datadir + write_systemd_service + start_service + create_admin_user + run_final_tests + rerun_plugin_install_if_available + print_summary +} +``` + +Calling `maybe_takeover_native_port` before `load_da_credentials` means that when the +takeover succeeds, `load_da_credentials` (which re-reads DA's `mysql.conf`) will correctly +see the *new* native port (3307), so the existing check in `validate_paths()` that rejects +`PORT == DA_PORT` continues to work unmodified — `PORT=3306` will correctly differ from +the now-updated `DA_PORT=3307`. + +- [ ] **Step 4: Run test to verify it passes** + +Run: `bash tests/install_db_native_takeover_wiring_test.sh` +Expected: `install_db_native_takeover_wiring_test: OK` + +- [ ] **Step 5: Run the full existing test suite to confirm no regression** + +Run each test file in `tests/*.sh` and confirm every one prints its own `OK` line, +including (but not limited to) `install_db_cli_test.sh` and all four +`native_mariadb_port_takeover_*_test.sh` files added in this plan. + +- [ ] **Step 6: Commit** + +```bash +git add scripts/install_db.sh tests/install_db_native_takeover_wiring_test.sh +git commit -m "feat: wire native MariaDB port takeover into install_db.sh when PORT=3306" +```