# Executes a command inside the MariaDB master pod. # $@ (...): command and arguments to execute. function mariadbMasterExec() { k3sRun exec pod/"$mariadbMasterKube"-0 -c "$mariadbMasterKube" -- "$@" } # Executes a command inside the MariaDB master pod with stdin attached (-i). # $@ (...): command and arguments to execute. function mariadbMasterExecI() { k3sRun exec -i pod/"$mariadbMasterKube"-0 -c "$mariadbMasterKube" -- "$@" } # Executes a command inside the MariaDB slave pod. # $@ (...): command and arguments to execute. function mariadbSlaveExec() { k3sRun exec pod/"$mariadbSlaveKube"-0 -c "$mariadbSlaveKube" -- "$@" } # Runs a SQL query as root against the specified MariaDB pod. # $1 (target): master|slave # $2 (query): SQL query. # [$3] (showColumn): pass "showColumn" to keep column headers. function mariadbRootQuery() { local target="$1" query="$2" [[ -n "$query" ]] || { appError "Query not specified"; return 1; } local execFn case "$target" in master) execFn=mariadbMasterExec ;; slave) execFn=mariadbSlaveExec ;; *) appError "Unknown target: $target"; return 1 ;; esac if [[ "${3-}" == "showColumn" ]]; then "$execFn" mariadb -uroot -p"$mariadbRootPass" -e "$query" else "$execFn" mariadb -uroot -p"$mariadbRootPass" -N -e "$query" fi } # Runs a SQL query as root against the MariaDB master pod. # $1 (query): SQL query. # [$2] (showColumn): pass "showColumn" to keep column headers. function mariadbMasterRootQuery() { mariadbRootQuery master "$@"; } # Runs a SQL query as root against the MariaDB slave pod. # $1 (query): SQL query. # [$2] (showColumn): pass "showColumn" to keep column headers. function mariadbSlaveRootQuery() { mariadbRootQuery slave "$@"; } # Converts a domain name into a MariaDB-safe identifier (max 60 chars). # $1 (domain): domain name. function mariadbDomain2id() { domainToRandom "$1" 60 } # Reads the MariaDB root password from the Kubernetes secret. function mariadbRootPassSecretGet() { k3sRun get secret "$mariadbKube-secret" -o jsonpath="{.data.root-password}" --ignore-not-found | base64 -d } # Saves the MariaDB root password from the Kubernetes secret to a local file. function mariadbRootPassSecretSave() { local password password="$(mariadbRootPassSecretGet)" [[ -n "$password" ]] && printf '%s\n' "$password" > "$appDataPath/$hostName.$mariadbKube" } # Lists user-created MariaDB databases (system schemas excluded). function mariadbDatabaseListGet() { local output error run output error mariadbMasterRootQuery "SHOW DATABASES;" || { appError "$error"; return 1; } printf '%s\n' "$output" | awk '!/^(information_schema|performance_schema|mysql|sys)$/' } # Lists MariaDB users with Host=% (root and replication_user excluded). function mariadbUserListGet() { local output error run output error mariadbMasterRootQuery "SELECT user FROM mysql.user WHERE Host = '%';" || { appError "$error"; return 1; } printf '%s\n' "$output" | awk '!/^(root|replication_user)$/' } # Creates or updates a MariaDB database and user with full privileges. # Revokes privileges from any other users previously assigned to the same database. # $1 (database): database name. # $2 (username): database username. # $3 (password): database user password. function mariadbDatabaseUserSet() { local database="$1" [[ -n "$database" ]] || { appError "Database not specified"; return 1; } local username="$2" [[ -n "$username" ]] || { appError "Username not specified"; return 1; } local password="$3" [[ -n "$password" ]] || { appError "Password not specified"; return 1; } mariadbMasterRootQuery "CREATE DATABASE IF NOT EXISTS \`$database\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER IF NOT EXISTS '$username'@'%' IDENTIFIED BY '$password'; ALTER USER '$username'@'%' IDENTIFIED BY '$password'; GRANT ALL PRIVILEGES ON \`$database\`.* TO '$username'@'%' WITH MAX_USER_CONNECTIONS 15;" || { appError "Create/update database user failed: database=$database username=$username" return 1 } local others others="$(mariadbMasterRootQuery "SELECT DISTINCT User, Host FROM mysql.db WHERE Db='$database' AND NOT (User='$username' AND Host='%');")" while IFS=$'\t' read -r u h; do [[ -z "$u" || -z "$h" ]] && continue mariadbMasterRootQuery "REVOKE ALL PRIVILEGES ON \`$database\`.* FROM '$u'@'$h';" || \ appError "Revoke privileges failed: database=$database user=$u host=$h" done <<< "$others" } # Checks whether a MariaDB database exists. # $1 (database): database name. function mariadbDatabaseExists() { local database="$1" [[ -n "$database" ]] || { appError "Database not specified"; return 1; } local out out="$(mariadbMasterRootQuery "SHOW DATABASES LIKE '$database';" 2>/dev/null | tail -n 1 | tr -d '\r')" [[ "$out" == "$database" ]] } # Removes a MariaDB database if it exists. # $1 (database): database name. function mariadbDatabaseRemove() { local database="$1" [[ -n "$database" ]] || { appError "Database not specified"; return 1; } mariadbMasterRootQuery "DROP DATABASE IF EXISTS \`$database\`;" } # Removes a MariaDB user from all hosts. # $1 (username): MariaDB username. function mariadbUserRemove() { local username="$1" [[ -n "$username" ]] || { appError "Username not specified"; return 1; } local hosts hosts="$(mariadbMasterRootQuery "SELECT Host FROM mysql.user WHERE User='$username';")" while read -r host; do mariadbMasterRootQuery "DROP USER IF EXISTS '$username'@'$host';" done <<< "$hosts" } # Removes all tables from a MariaDB database. # $1 (database): database name. # $2 (username): database username. # $3 (password): database user password. function mariadbDatabaseClean() { local database="$1" [[ -n "$database" ]] || { appError "Database not specified"; return 1; } local username="$2" [[ -n "$username" ]] || { appError "Username not specified"; return 1; } local password="$3" [[ -n "$password" ]] || { appError "Password not specified"; return 1; } local output error run output error mariadbMasterExec mariadb -u "$username" -p"$password" -N -e "SELECT CONCAT('DROP TABLE \`', table_name, '\`;') FROM information_schema.tables WHERE table_schema = '$database';" || { appError "$error"; return 1; } run output error mariadbMasterExec mariadb -u "$username" -p"$password" -e "USE \`$database\`; SET FOREIGN_KEY_CHECKS = 0; $output SET FOREIGN_KEY_CHECKS = 1;" || { appError "$error"; return 1; } } # Base: runs mariadb-dump via the given exec function. # $1 (execFn): mariadbMasterExec|mariadbSlaveExec. # $2 (username), $3 (password), $4 (database|all). # [$5] (file): auto-generated if omitted. # [$6+] (...): optional mariadb-dump parameters. function mariadbExport() { local execFn="$1" [[ -n "$execFn" ]] || { appError "Exec function not specified"; return 1; } shift local username="$1" [[ -n "$username" ]] || { appError "Username not specified"; return 1; } shift local password="$1" [[ -n "$password" ]] || { appError "Password not specified"; return 1; } shift local database="$1" [[ -n "$database" ]] || { appError "Database not specified"; return 1; } shift local file="$1" if [[ -z "$file" ]]; then [[ "$database" == "all" ]] \ && file="$appDataPath/$mariadbMasterKube/${appDate}_${appTime}_full.sql.tar.gz" \ || file="$appDataPath/$mariadbMasterKube/${appDate}_${appTime}_$database.sql.tar.gz" fi shift local -a params if [[ $# -gt 0 ]]; then params=("$@") local haveAll=false haveDatabases=false havePositional=false for a in "${params[@]}"; do case "$a" in --all-databases|-A) haveAll=true ;; --databases|-B) haveDatabases=true ;; -*) ;; *) havePositional=true ;; esac done if ! $haveAll && ! $haveDatabases && ! $havePositional; then [[ "$database" == "all" ]] && params+=(--all-databases) || params+=("$database") fi else if [[ "$database" == "all" ]]; then params=(--all-databases --single-transaction --quick --master-data=2 --routines --events) else params=("$database" --single-transaction --quick --routines --events) fi fi local filePath fileBase fileSql filePath=$(dirname -- "$file") fileBase=$(basename -- "$file") case "$fileBase" in *.zip) fileSql="${fileBase%.zip}" ;; *.tar.gz) fileSql="${fileBase%.tar.gz}" ;; *) fileSql="$fileBase" ;; esac mkdir -p -- "$filePath" || { appError "Failed to create directory: $filePath"; return 1; } rm -f -- "$filePath/$fileSql" "$filePath/$fileBase" &>/dev/null local output error runShell output error "$execFn" mariadb-dump -u "$username" -p"$password" "${params[@]}" ">" "$filePath/$fileSql" || { rm -f -- "$filePath/$fileSql" &>/dev/null appError "Error creating dump: ${error:-$output}" return 1 } case "$fileBase" in *.zip|*.tar.gz|*.tgz) runError error archiveCreate "$filePath/$fileSql" "$file" || { rm -f -- "$filePath/$fileSql" "$filePath/$fileBase" &>/dev/null appError "Error creating archive dump: $error" return 1 } rm -f -- "$filePath/$fileSql" &>/dev/null ;; esac printf '%s' "$file" } # Exports from the master pod. # $1 (username), $2 (password), $3 (database|all), [$4] (file), [$5+] (params) function mariadbMasterExport() { mariadbExport mariadbMasterExec "$@" } # Exports using root credentials. # $1 (execFn), $2 (database|all), [$3] (file), [$4+] (params) function mariadbRootExport() { mariadbExport "$1" root "$mariadbRootPass" "${@:2}" } # Exports from master using root credentials. # [$1] (database|all), [$2] (file), [$3+] (params) function mariadbMasterRootExport() { local target="${1:-all}" local file="${2:-$appDataPath/$mariadbMasterKube/${appDate}_${appTime}_full.sql.tar.gz}" mariadbRootExport mariadbMasterExec "$target" "$file" ${@:3} } # Exports from slave using root credentials. # [$1] (database|all), [$2] (file) function mariadbSlaveRootExport() { local target="${1:-all}" local file="${2:-$appDataPath/$mariadbSlaveKube/${appDate}_${appTime}_full.sql.tar.gz}" mariadbRootExport mariadbSlaveExec "$target" "$file" --all-databases --single-transaction --quick --routines --events } # Imports a MariaDB dump into a database. # Supports plain SQL, .zip and .tar.gz archives (must contain exactly one file). # $1 (database|all): target database, or "all" to import without selecting a database. # $2 (username): username used for import. # $3 (password): password used for import. # $4 (file): source dump file. function mariadbMasterImport() { local database="$1" [[ -n "$database" ]] || { appError "Database not specified"; return 1; } local username="$2" [[ -n "$username" ]] || { appError "Username not specified"; return 1; } local password="$3" [[ -n "$password" ]] || { appError "Password not specified"; return 1; } local file="$4" [[ -n "$file" ]] || { appError "File not specified"; return 1; } [[ -f "$file" ]] || { appError "File not found: $file"; return 1; } local tmpFile if [[ "$file" == *.zip ]]; then local entriesRaw entriesRaw="$(unzip -Z1 "$file" 2>/dev/null)" || { appError "Not a valid zip archive: $file"; return 1; } local -a entries=() mapfile -t entries < <(printf '%s\n' "$entriesRaw" | sed '/\/$/d') [[ ${#entries[@]} -eq 1 ]] || { appError "Archive must contain exactly one file: $file"; return 1; } tmpFile="$(mktemp)" || { appError "Failed to create temporary file"; return 1; } unzip -p "$file" "${entries[0]}" > "$tmpFile" 2>/dev/null || { rm -f -- "$tmpFile" appError "Failed to extract ZIP archive" return 1 } elif [[ "$file" == *.tar.gz ]]; then local entriesRaw entriesRaw="$(tar -tzf "$file" 2>/dev/null)" || { appError "Not a valid tar.gz archive: $file"; return 1; } local -a entries=() mapfile -t entries < <(printf '%s\n' "$entriesRaw" | sed '/\/$/d') [[ ${#entries[@]} -eq 1 ]] || { appError "Archive must contain exactly one file: $file"; return 1; } tmpFile="$(mktemp)" || { appError "Failed to create temporary file"; return 1; } tar -xOzf "$file" "${entries[0]}" > "$tmpFile" 2>/dev/null || { rm -f -- "$tmpFile" appError "Failed to extract tar.gz archive" return 1 } else tmpFile="$file" fi if [[ ! -s "$tmpFile" ]]; then [[ "$tmpFile" == "$file" ]] || rm -f -- "$tmpFile" appError "The SQL dump is empty" return 1 fi local -a mariadbCmd=(mariadbMasterExecI mariadb -u "$username" -p"$password") [[ "$database" != "all" ]] && mariadbCmd+=("$database") local output error runShell output error "${mariadbCmd[@]}" "<" "$tmpFile" ">" /dev/null || { [[ "$tmpFile" == "$file" ]] || rm -f -- "$tmpFile" appError "Import failed: ${error:-$output}" return 1 } [[ "$tmpFile" == "$file" ]] || rm -f -- "$tmpFile" } # Imports a MariaDB dump using root credentials. # $1 (database|all): target database, or "all". # $2 (file): source dump file. function mariadbMasterRootImport() { mariadbMasterImport "$1" root "$mariadbRootPass" "${@:2}" } # Creates or updates the MariaDB database and user for a site. # $1 (domain): site domain name. function mariadbConfigRebuild() { local domain domain=$(domainPrepare "$1") domainCheck "$domain" || return 1 local databaseName databaseUser databasePass databaseName=$(siteConfigGetOrSet "$domain" "databaseName" "$(mariadbDomain2id "$domain")") databaseUser=$(siteConfigGetOrSet "$domain" "databaseUser" "$(mariadbDomain2id "$domain")") databasePass=$(siteConfigGetOrCreate "$domain" "databasePass") mariadbDatabaseUserSet "$databaseName" "$databaseUser" "$databasePass" || return 1 }