Files
alt-mysql/docs/superpowers/plans/2026-07-05-native-port-takeover.md
Marek Miklewicz c59d5b8c77 docs: correct plan's rollback trap design from ERR to EXIT
die() calls exit directly; bash's ERR trap does not fire on an explicit
exit, only when a command's non-zero status propagates through normal
control flow. Verified empirically during Task 4 implementation - switched
to a ROLLBACK_ARMED-guarded EXIT trap, which fires on any shell exit.
2026-07-05 21:49:51 +02:00

45 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 and the existing log()/die() pattern. The rollback design uses a trap ... EXIT (guarded by a ROLLBACK_ARMED flag), not trap ... ERRdie() 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 <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"

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 exitdie() 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
#!/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_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:

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"