Files
alt-mysql/docs/superpowers/plans/2026-07-05-native-port-takeover.md
T
2026-07-05 21:05:09 +02:00

44 KiB

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 <original> (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 <MARIADB_VERSION> [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 <MARIADB_VERSION> <PORT> followed by three examples and a list of allowed branches) and replace it with:

usage() {
    cat <<EOF
Użycie:
  $0 <MARIADB_VERSION> [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:

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:

printf '%s\n' "$NO_ARGS_OUTPUT" | grep -Fq '<MARIADB_VERSION> <PORT>' \
  || fail "install_db.sh usage does not show required version and port"

Replace it with:

printf '%s\n' "$NO_ARGS_OUTPUT" | grep -Fq '<MARIADB_VERSION> \[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):

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
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:

#!/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
#!/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
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:

#!/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" <<EOF
#!/bin/bash
echo "\$*" >> "$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:

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
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:

#!/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" <<EOF
#!/bin/bash
echo "\$*" >> "$dir/da_calls.log"
exit 0
EOF
  chmod 755 "$dir/bin/da"

  cat > "$dir/bin/mysql" <<EOF
#!/bin/bash
CURRENT_PORT_FILE="$dir/current_port"
requested_port=""
for arg in "\$@"; do
  case "\$arg" in
    --port=*) requested_port="\${arg#--port=}" ;;
  esac
done
if [ "\$requested_port" = "\$(cat "\$CURRENT_PORT_FILE" 2>/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" <<EOF
#!/bin/bash
# mysql_service_name() (mysql_common.sh) probes "mysqld", then "mariadb", then
# "mysql" via 'systemctl list-unit-files <name>.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" <<EOF
#!/bin/bash
current="\$(cat "$dir/current_port" 2>/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:

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:

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 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
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:

#!/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" <<EOF
#!/bin/bash
echo "CALLED" >> "$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" <<EOF
#!/bin/bash
echo "CALLED-FAIL" >> "$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):

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):

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:

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
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"