# 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` and the existing `log()`/`die()` pattern. The rollback design uses a `trap ... EXIT` (guarded by a `ROLLBACK_ARMED` flag), not `trap ... ERR` — `die()` calls `exit` directly, and bash's `ERR` trap does not fire on an explicit `exit`, only on a command's non-zero status propagating through normal control flow. This was verified empirically during Task 4's implementation. - 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" ``` Note: the three "expected to fail" calls above (`check_os_guard` for CloudLinux and AlmaLinux 7, and `preflight_new_port_free` for an occupied port) are each wrapped in a subshell `( ... )` rather than called bare. This test sources the script directly rather than executing it as a subprocess, so a bare failing call's `die()` → `exit 1` would terminate the whole sourcing shell (this test script itself), never letting the `if` statement observe the failure. Wrapping the call in `( ... )` confines that `exit` to the subshell, so `if ( failing_call ); then` correctly observes a non-zero status without killing the test. Do not "fix" this by changing `die()` to use `return` instead of `exit` — `die()` is also used when this script is executed directly (not sourced), where `exit` is the correct, intended behavior. - [ ] **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_ARMED=0 rollback_service_state() { [ "$ROLLBACK_ARMED" -eq 1 ] || return 0 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 # Deliberately an EXIT trap, not an ERR trap. die() (used by every failure # path below) calls `exit` directly, and bash's ERR trap does not fire for # a function that terminates via an explicit `exit` call - it only fires # when a command's non-zero status propagates through normal control flow. # An EXIT trap fires on *any* shell exit (explicit exit, errexit-triggered # exit, or falling off the end of the script), which is what "any failure # from this point on triggers rollback" actually requires. ROLLBACK_ARMED # guards it so it is a no-op before this point and after a successful run. ROLLBACK_ARMED=1 trap 'rollback_service_state' EXIT disable_custombuild_mysql_management apply_config_mutations restart_native_service verify_native_listening_new_port restart_directadmin_service verify_da_connectivity_new_port ROLLBACK_ARMED=0 trap - EXIT 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" ```