#!/bin/bash
#Nifty: Check Percona MySQL for upgrade incompatibilities with MySQL Shell

# shellcheck source=../../lib/universal/niftylib.sh
source <(curl -fSs https://nifty.sh/lib/universal/niftylib.sh)

# Requirements
require_root
require_64bit
require_os alma8 alma9 alma10
require_cmd curl dnf mysql

PERCONA_URL="https://repo.percona.com"
MYSQLSH_PACKAGE="percona-mysql-shell"
REMOVE_MYSQLSH=""

WORKDIR="$(mktemp -d -t mysql-upgrade-check-XXXXXXXXXX)"

cleanup() {
  if [ -n "$REMOVE_MYSQLSH" ]; then
    ce "Removing MySQL Shell again"
    dnf remove -y -q "$MYSQLSH_PACKAGE"
  fi
  rm -rf "$WORKDIR"
}
trap cleanup EXIT

CLIENT_CNF="$WORKDIR/client.cnf"

MYSQL_USER=""
MYSQL_PASSWORD=""

# Option file values may contain "#", which would otherwise start a comment
escape_cnf_value() {
  local VALUE="$1"
  VALUE="${VALUE//\\/\\\\}"
  VALUE="${VALUE//\"/\\\"}"
  printf '"%s"' "$VALUE"
}

write_client_cnf() {
  with_umask 077 tee "$CLIENT_CNF" > /dev/null <<EOF
[client]
password = $(escape_cnf_value "$MYSQL_PASSWORD")
EOF
}

prompt_password() {
  local RESPONSE
  printf '%s' "$(tput bold)MySQL password for $MYSQL_USER$(tput sgr0) > " >&2
  read -rs RESPONSE
  echo >&2
  echo "$RESPONSE"
}

ce "Looking for MySQL credentials"
if [ -f "/sbin/plesk" ] && [ -f "/etc/psa/.psa.shadow" ]; then
  echo "Found Plesk, using the admin password from /etc/psa/.psa.shadow"
  MYSQL_USER="admin"
  MYSQL_PASSWORD="$(cat /etc/psa/.psa.shadow)"
elif [ -f "/root/.mysql" ]; then
  echo "Found /root/.mysql, using it as the root password"
  MYSQL_USER="root"
  MYSQL_PASSWORD="$(cat /root/.mysql)"
elif [ -f "/root/.my.cnf" ]; then
  echo "Found /root/.my.cnf, using the password from it"
  # MySQL Shell ignores option files, so pick the password out ourselves
  MYSQL_PASSWORD="$(grep -ioP '^[[:space:]]*password[[:space:]]*=[[:space:]]*\K.*' /root/.my.cnf \
    | head -n1 | sed -e 's/[[:space:]]*$//' -e 's/^"\(.*\)"$/\1/' -e "s/^'\(.*\)'$/\1/")"
else
  echo "No stored credentials found"
fi

[ -z "$MYSQL_USER" ] && MYSQL_USER="root"

if [ -z "$MYSQL_PASSWORD" ]; then
  if ! is_interactive; then
    err "No MySQL password could be determined and there is no terminal to ask on."
    exit 1
  fi
  MYSQL_PASSWORD="$(prompt_password)"
fi

run_mysql() {
  mysql --defaults-extra-file="$CLIENT_CNF" --user="$MYSQL_USER" --connect-timeout=5 "$@"
}

ce "Connecting to MySQL"
write_client_cnf
if ! run_mysql -NBe 'SELECT 1' > /dev/null 2>&1; then
  err "Could not connect to MySQL as $MYSQL_USER."
  exit 1
fi

# MySQL Shell ignores option files and would otherwise look for /tmp/mysql.sock
MYSQL_SOCKET="$(run_mysql -NBe 'SELECT @@socket' 2>/dev/null)"
if [ -S "$MYSQL_SOCKET" ]; then
  CONNECT_ARGS=("--socket=$MYSQL_SOCKET")
else
  CONNECT_ARGS=("--host=127.0.0.1")
fi

ce "Detecting the installed MySQL release"
MYSQL_VERSION="$(run_mysql -NBe 'SELECT VERSION()' 2>/dev/null | grep -oP '^[0-9]+\.[0-9]+')"
if [ -z "$MYSQL_VERSION" ]; then
  MYSQL_VERSION="$(mysqld --version 2>/dev/null | grep -oP 'Ver \K[0-9]+\.[0-9]+')"
fi
if [ -z "$MYSQL_VERSION" ]; then
  MYSQL_VERSION="$(rpm -q --queryformat '%{VERSION}\n' percona-server-server 2>/dev/null | grep -oP '^[0-9]+\.[0-9]+' | head -n1)"
fi

if [ -n "$MYSQL_VERSION" ]; then
  echo "This server runs MySQL $MYSQL_VERSION"
else
  err "Failed to detect the installed MySQL release."
fi

MYSQLSH_VERSION="${NIFTY_MYSQLSH_VERSION:-}"

ce "Retrieving the list of Percona LTS releases"
# 8.0 predates Percona's -lts naming but is the only valid step up from 5.7
declare -A PERCONA_REPOS
MYSQLSH_VERSIONS=()
while read -r REPO; do
  [ -z "$REPO" ] && continue
  VERSION="$(sed -e 's/-lts$//' -e 's/^\([0-9]\)/\1./' <<< "$REPO")"
  PERCONA_REPOS[$VERSION]="ps-$REPO"
  MYSQLSH_VERSIONS+=("$VERSION")
done <<< "$(curl -Ssf "$PERCONA_URL/" | grep -oP 'href="ps-\K(80|[0-9]+-lts)(?=/")' | sort -V | uniq)"
if [ "${#MYSQLSH_VERSIONS[@]}" = "0" ]; then
  err "Failed to get the repository index from $PERCONA_URL"
  exit 1
fi

# The upgrade checker can only target releases up to its own version, so we want
# the oldest LTS shell that is still newer than what is installed
if [ -z "$MYSQLSH_VERSION" ] && [ -n "$MYSQL_VERSION" ]; then
  for VERSION in "${MYSQLSH_VERSIONS[@]}"; do
    if compare_versions "$VERSION" ">" "$MYSQL_VERSION"; then
      MYSQLSH_VERSION="$VERSION"
      break
    fi
  done
fi

if [ -n "$MYSQLSH_VERSION" ] && [ -z "${PERCONA_REPOS[$MYSQLSH_VERSION]:-}" ]; then
  err "MySQL Shell $MYSQLSH_VERSION is not an available LTS release."
  MYSQLSH_VERSION=""
fi

if [ -z "$MYSQLSH_VERSION" ]; then
  if [ -n "$MYSQL_VERSION" ]; then
    err "No LTS release newer than the installed MySQL $MYSQL_VERSION is available from Percona."
  fi

  if ! is_interactive; then
    err "Set the target release with the NIFTY_MYSQLSH_VERSION environment variable. Available: $(implode ", " "${MYSQLSH_VERSIONS[@]}")"
    exit 1
  fi

  MYSQLSH_VERSION="$(prompt_select_num_value "Which MySQL Shell release would you like to check against?" "${MYSQLSH_VERSIONS[@]}")"
fi

install_mysqlsh() {
  local REPO_ID="nifty-percona-mysqlsh"
  # A command-line repo leaves no config behind, and gpgkey keeps signature checking on
  dnf install -y -q \
    --repofrompath="$REPO_ID,$PERCONA_URL/${PERCONA_REPOS[$MYSQLSH_VERSION]}/yum/release/$OS_VERSION/RPMS/x86_64" \
    --setopt="$REPO_ID.gpgkey=$PERCONA_URL/yum/PERCONA-PACKAGING-KEY" \
    "$MYSQLSH_PACKAGE-$MYSQLSH_VERSION.*"
}

INSTALLED_MYSQLSH_VERSION=""
if has_cmd mysqlsh; then
  INSTALLED_MYSQLSH_VERSION="$(mysqlsh --version 2>/dev/null | grep -oP 'Ver \K[0-9]+\.[0-9]+')"
fi

# A newer shell can still check against an older target, so never downgrade
if [ -z "$INSTALLED_MYSQLSH_VERSION" ]; then
  ce "Installing MySQL Shell $MYSQLSH_VERSION"
  REMOVE_MYSQLSH="1"
  if ! install_mysqlsh; then
    err "Failed to install MySQL Shell $MYSQLSH_VERSION"
    exit 1
  fi
elif compare_versions "$INSTALLED_MYSQLSH_VERSION" ">=" "$MYSQLSH_VERSION"; then
  ce "Using the already installed MySQL Shell $INSTALLED_MYSQLSH_VERSION"
elif ! has_package "$MYSQLSH_PACKAGE"; then
  err "MySQL Shell $INSTALLED_MYSQLSH_VERSION is installed from a package other than $MYSQLSH_PACKAGE, so it can't be upgraded to $MYSQLSH_VERSION."
  exit 1
else
  if ! is_interactive; then
    err "The installed MySQL Shell $INSTALLED_MYSQLSH_VERSION is too old to check against $MYSQLSH_VERSION, and there is no terminal to confirm an upgrade on."
    exit 1
  fi
  if ! prompt_confirm "The installed MySQL Shell $INSTALLED_MYSQLSH_VERSION is too old to check against $MYSQLSH_VERSION. Upgrade it?"; then
    exit 1
  fi
  ce "Upgrading MySQL Shell to $MYSQLSH_VERSION"
  if ! install_mysqlsh; then
    err "Failed to upgrade MySQL Shell to $MYSQLSH_VERSION"
    exit 1
  fi
fi

CHECK_ARGS=("--target-version=$MYSQLSH_VERSION" "--output-format=TEXT")
for CONFIG_PATH in /etc/my.cnf /etc/mysql/my.cnf; do
  if [ -f "$CONFIG_PATH" ]; then
    CHECK_ARGS+=("--config-path=$CONFIG_PATH")
    break
  fi
done

run_check() {
  # --mysql forces the classic protocol, as the X plugin is usually not enabled.
  # --passwords-from-stdin still prints its password prompt, so strip that.
  printf '%s\n' "$MYSQL_PASSWORD" | mysqlsh \
    --quiet-start=1 \
    --mysql \
    --passwords-from-stdin \
    --user="$MYSQL_USER" \
    "${CONNECT_ARGS[@]}" \
    -- util check-for-server-upgrade "${CHECK_ARGS[@]}" 2>&1 \
    | sed -u "s/^Please provide the password for '[^']*': //"
  return "${PIPESTATUS[1]}"
}

while :; do
  ce "Checking MySQL ${MYSQL_VERSION:-unknown} for upgrade to $MYSQLSH_VERSION"
  echo
  run_check
  RESULT="$?"

  echo
  if [ "$RESULT" = "0" ]; then
    ce "No upgrade blockers were found."
  else
    err "The upgrade checker reported problems (exit code $RESULT). Review the output above before upgrading."
  fi

  is_interactive || break

  NEXT_OPTIONS=("Re-check")
  if [ -n "$REMOVE_MYSQLSH" ]; then
    NEXT_OPTIONS+=("Uninstall MySQL Shell and exit" "Exit without uninstalling MySQL Shell")
  else
    NEXT_OPTIONS+=("Exit")
  fi

  echo
  case "$(prompt_select_num_value "What would you like to do?" "${NEXT_OPTIONS[@]}")" in
    "Re-check") echo ;;
    "Exit without uninstalling MySQL Shell") REMOVE_MYSQLSH=""; break ;;
    *) break ;;
  esac
done

exit "$RESULT"
