From f3c6fd2781f88164a64ef762174ec1ebe009ac88 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Mon, 31 Jan 2022 15:27:25 +1100 Subject: [PATCH] MDEV-25434: mariadb container to have HEALTHCHECK Adds a healthcheck script that gives the user few basics options for health checks include: * connect * innodb_buffer_pool_loaded * galeraonline * replication (io/sql threads and/or delay) * mariadbupgrade (unhealthy if in progress) Reviewed thanks to Daniel Rudolf (@PhrozenByte) and Faustin Lammler (@fauust) --- 10.2/Dockerfile | 1 + 10.2/healthcheck.sh | 306 ++++++++++++++++++++++++++++++++++++++++++++ 10.3/Dockerfile | 1 + 10.3/healthcheck.sh | 306 ++++++++++++++++++++++++++++++++++++++++++++ 10.4/Dockerfile | 1 + 10.4/healthcheck.sh | 306 ++++++++++++++++++++++++++++++++++++++++++++ 10.5/Dockerfile | 1 + 10.5/healthcheck.sh | 306 ++++++++++++++++++++++++++++++++++++++++++++ 10.6/Dockerfile | 1 + 10.6/healthcheck.sh | 306 ++++++++++++++++++++++++++++++++++++++++++++ 10.7/Dockerfile | 1 + 10.7/healthcheck.sh | 306 ++++++++++++++++++++++++++++++++++++++++++++ 10.8/Dockerfile | 1 + 10.8/healthcheck.sh | 306 ++++++++++++++++++++++++++++++++++++++++++++ Dockerfile.template | 1 + healthcheck.sh | 306 ++++++++++++++++++++++++++++++++++++++++++++ update.sh | 3 +- 17 files changed, 2458 insertions(+), 1 deletion(-) create mode 100755 10.2/healthcheck.sh create mode 100755 10.3/healthcheck.sh create mode 100755 10.4/healthcheck.sh create mode 100755 10.5/healthcheck.sh create mode 100755 10.6/healthcheck.sh create mode 100755 10.7/healthcheck.sh create mode 100755 10.8/healthcheck.sh create mode 100755 healthcheck.sh diff --git a/10.2/Dockerfile b/10.2/Dockerfile index 0981e0a..bdcf21b 100644 --- a/10.2/Dockerfile +++ b/10.2/Dockerfile @@ -131,6 +131,7 @@ RUN set -ex; \ VOLUME /var/lib/mysql +COPY healthcheck.sh /usr/local/bin/healthcheck.sh COPY docker-entrypoint.sh /usr/local/bin/ RUN ln -s usr/local/bin/docker-entrypoint.sh / # backwards compat ENTRYPOINT ["docker-entrypoint.sh"] diff --git a/10.2/healthcheck.sh b/10.2/healthcheck.sh new file mode 100755 index 0000000..32dfec9 --- /dev/null +++ b/10.2/healthcheck.sh @@ -0,0 +1,306 @@ +#!/bin/bash +# +# Healthcheck script for MariaDB +# +# Runs various tests on the MariaDB server to check its health. Pass the tests +# to run as arguments. If all tests succeed, the server is considered healthy, +# otherwise it's not. +# +# Arguments are processed in strict order. Set replication_* options before +# the --replication option. This allows a different set of replication checks +# on different connections. +# +# --su{=|mysql} is option to run the healthcheck as a different unix user. +# Useful if mysql@localhost user exists with unix socket authentication +# Using this option disregards previous options/tests, so should usually be the +# first option. +# +# Some tests require SQL privileges. +# +# TEST GRANTS REQUIRED +# connect none* +# innodb_buffer_pool_loaded USAGE +# galera_online USAGE +# replication SUPER or REPLICATION_CLIENT or REPLICA MONITOR (10.5+) +# mariadbupgrade none, however unix user permissions on datadir +# +# The SQL user used is the default for the mysql client. This can be the unix user +# if no user(or password) is set in the [mariadb-client] section of a configuration +# file. --defaults-{file,extra-file,group-suffix} can specify a file/configuration +# different from elsewhere. +# +# Note * though denied error message will result in error log without +# any permissions. + +set -eo pipefail + +_process_sql() +{ + mysql ${nodefaults:+--no-defaults} \ + ${def['file']:+--defaults-file=${def['file']}} \ + ${def['extra_file']:+--defaults-extra-file=${def['extra_file']}} \ + ${def['group_suffix']:+--defaults-group-suffix=${def['group_suffix']}} \ + -B "$@" +} + +# TESTS + + +# CONNECT +# +# Tests that a connection can be made over TCP, the final state +# of the entrypoint and is listening. The authentication used +# isn't tested. +connect() +{ + set +e +o pipefail + mysql ${nodefaults:+--no-defaults} \ + ${def['file']:+--defaults-file=${def['file']}} \ + ${def['extra_file']:+--defaults-extra-file=${def['extra_file']}} \ + ${def['group_suffix']:+--defaults-group-suffix=${def['group_suffix']}} \ + -h localhost --protocol tcp -e 'select 1' 2>&1 \ + | grep -qF "Can't connect" + local ret=${PIPESTATUS[1]} + set -eo pipefail + if (( "$ret" == 0 )); then + # grep Matched "Can't connect" so we fail + return 1 + fi + return 0 +} + +# INNODB_BUFFER_POOL_LOADED +# +# Tests the load of the innodb buffer pool as been complete +# implies innodb_buffer_pool_load_at_startup=1 (default), or if +# manually SET innodb_buffer_pool_load_now=1 +innodb_buffer_pool_loaded() +{ + local s + s=$(_process_sql --skip-column-names -e 'select VARIABLE_VALUE from information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME="Innodb_buffer_pool_load_status"') + if [[ $s =~ 'load completed' ]]; then + return 0 + fi + return 1 +} + +# GALERAONLINE +# +# Tests that the galera node is in the SYNCed state +galeraonline() +{ + local s + s=$(_process_sql --skip-column-names -e 'select VARIABLE_VALUE from information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME="WSREP_LOCAL_STATE"') + # 4 from https://galeracluster.com/library/documentation/node-states.html#node-state-changes + # not https://xkcd.com/221/ + if [[ $s -eq 4 ]]; then + return 0 + fi + return 1 +} + +# REPLICATION +# +# Tests the replication has the required set of functions: +# --replication_all -> Checks all replication sources +# --replication_name=n -> sets the multisource connection name tested +# --replication_io -> IO thread is running +# --replication_sql -> SQL thread is running +# --replication_seconds_behind_master=n -> less than or equal this seconds of delay +# --replication_sql_remaining_delay=n -> less than or equal this seconds of remaining delay +# (ref: https://mariadb.com/kb/en/delayed-replication/) +replication() +{ + # SHOW REPLICA available 10.5+ + # https://github.com/koalaman/shellcheck/issues/2383 + # shellcheck disable=SC2016,SC2026 + _process_sql -e "show ${repl['all']:+all} slave${repl['all']:+s} ${repl['name']:+'${repl['name']}'} status\G" | \ + { + # required for trim of leading space. + shopt -s extglob + # Row header + read -t 5 -r + # read timeout + [ $? -gt 128 ] && return 1 + while IFS=":" read -t 1 -r n v; do + # Trim leading space + n=${n##+([[:space:]])} + # Leading space on all values by the \G format needs to be trimmed. + v=${v:1} + case "$n" in + Slave_IO_Running) + if [ -n "${repl['io']}" ] && [ "$v" = 'No' ]; then + return 1 + fi + ;; + Slave_SQL_Running) + if [ -n "${repl['sql']}" ] && [ "$v" = 'No' ]; then + return 1 + fi + ;; + Seconds_Behind_Master) + # A NULL value is the IO thread not running: + if [ -n "${repl['seconds_behind_master']}" ] && + { [ "$v" = NULL ] || + (( "${repl['seconds_behind_master']}" < "$v" )); }; then + return 1 + fi + ;; + SQL_Remaining_Delay) + # Unlike Seconds_Behind_Master, sql_remaining_delay will hit NULL + # once replication is caught up - https://mariadb.com/kb/en/delayed-replication/ + if [ -n "${repl['sql_remaining_delay']}" ] && + [ "$v" != NULL ] && + (( "${repl['sql_remaining_delay']}" < "$v" )); then + return 1 + fi + ;; + esac + done + # read timeout + [ $? -gt 128 ] && return 1 + return 0 + } + return $? +} + +# mariadbupgrade +# +# Test the lock on the file /var/lib/mysql_upgrade_info +# https://jira.mariadb.org/browse/MDEV-27068 +mariadbupgrade() +{ + local f="$datadir/mysql_upgrade_info" + if [ -r "$f" ]; then + flock --exclusive --nonblock -n 9 9<"$f" + return $? + fi + return 0 +} + + +# MAIN + +if [ $# -eq 0 ]; then + echo "At least one argument required" >&2 + exit 1 +fi + +# Global variables used by tests +declare -A repl +declare -A def +nodefaults= +datadir=/var/lib/mysql + +_repl_param_check() +{ + case "$1" in + seconds_behind_master) ;& + sql_remaining_delay) + if [ -z "${repl['io']}" ]; then + repl['io']=1 + echo "Forcing --replication_io=1, $1 requires IO thread to be running" >&2 + fi + ;; + all) + if [ -n "${repl['name']}" ]; then + unset 'repl[name]' + echo "Option --replication_all incompatible with specied source --replication_name, clearing replication_name" >&2 + fi + ;; + name) + if [ -n "${repl['all']}" ]; then + unset 'repl[all]' + echo "Option --replication_name incompatible with --replication_all, clearing replication_all" >&2 + fi + ;; + esac +} + +_test_exists() { + declare -F "$1" + return $? +} + +# Marks the end of mysql -> mariadb name changes in 10.6+ +#ENDOFSUBSTITUTIONS +while [ $# -gt 0 ]; do + case "$1" in + --su=*) + u="${1#*-}" + shift + exec gosu "${u}" "${BASH_SOURCE[0]}" "$@" + ;; + --su-mysql) + shift + exec gosu mysql "${BASH_SOURCE[0]}" "$@" + ;; + --replication_*=*) + # Change the n to what is between _ and = and make lower case + n=${1#*_} + n=${n%%=*} + n=${n,,*} + # v is after the = + v=${1#*=} + repl[$n]=$v + _repl_param_check "$n" + ;; + --replication_*) + # Without =, look for a non --option next as the value, + # otherwise treat it as an "enable", just equate to 1. + # Clearing option is possible with "--replication_X=" + n=${1#*_} + n=${n,,*} + if [ "${2:0:2}" == '--' ]; then + repl[$n]=1 + else + repl[$n]=$2 + shift + fi + _repl_param_check "$n" + ;; + --datadir=*) + datadir=${1#*=} + ;; + --no-defaults) + unset def + nodefaults=1 + ;; + --defaults-file=*|--defaults-extra-file=*|--defaults-group-suffix=*) + n=${1:11} # length --defaults- + n=${n%%=*} + n=${n//-/_} + # v is after the = + v=${1#*=} + def[$n]=$v + nodefaults= + ;; + --defaults-file|--defaults-extra-file|--defaults-group-suffix) + n=${1:11} # length --defaults- + n=${n//-/_} + if [ "${2:0:2}" == '--' ]; then + def[$n]="" + else + def[$n]=$2 + shift + fi + nodefaults= + ;; + --*) + test=${1#--} + ;; + *) + echo "Unknown healthcheck option $1" >&2 + exit 1 + esac + if [ -n "$test" ]; then + if ! _test_exists "$test" ; then + echo "healthcheck unknown test '$test'" >&2 + exit 1 + elif ! "$test"; then + echo "healthcheck $test failed" >&2 + exit 1 + fi + fi + shift +done diff --git a/10.3/Dockerfile b/10.3/Dockerfile index 27ae9fa..4da0a0f 100644 --- a/10.3/Dockerfile +++ b/10.3/Dockerfile @@ -131,6 +131,7 @@ RUN set -ex; \ VOLUME /var/lib/mysql +COPY healthcheck.sh /usr/local/bin/healthcheck.sh COPY docker-entrypoint.sh /usr/local/bin/ RUN ln -s usr/local/bin/docker-entrypoint.sh / # backwards compat ENTRYPOINT ["docker-entrypoint.sh"] diff --git a/10.3/healthcheck.sh b/10.3/healthcheck.sh new file mode 100755 index 0000000..32dfec9 --- /dev/null +++ b/10.3/healthcheck.sh @@ -0,0 +1,306 @@ +#!/bin/bash +# +# Healthcheck script for MariaDB +# +# Runs various tests on the MariaDB server to check its health. Pass the tests +# to run as arguments. If all tests succeed, the server is considered healthy, +# otherwise it's not. +# +# Arguments are processed in strict order. Set replication_* options before +# the --replication option. This allows a different set of replication checks +# on different connections. +# +# --su{=|mysql} is option to run the healthcheck as a different unix user. +# Useful if mysql@localhost user exists with unix socket authentication +# Using this option disregards previous options/tests, so should usually be the +# first option. +# +# Some tests require SQL privileges. +# +# TEST GRANTS REQUIRED +# connect none* +# innodb_buffer_pool_loaded USAGE +# galera_online USAGE +# replication SUPER or REPLICATION_CLIENT or REPLICA MONITOR (10.5+) +# mariadbupgrade none, however unix user permissions on datadir +# +# The SQL user used is the default for the mysql client. This can be the unix user +# if no user(or password) is set in the [mariadb-client] section of a configuration +# file. --defaults-{file,extra-file,group-suffix} can specify a file/configuration +# different from elsewhere. +# +# Note * though denied error message will result in error log without +# any permissions. + +set -eo pipefail + +_process_sql() +{ + mysql ${nodefaults:+--no-defaults} \ + ${def['file']:+--defaults-file=${def['file']}} \ + ${def['extra_file']:+--defaults-extra-file=${def['extra_file']}} \ + ${def['group_suffix']:+--defaults-group-suffix=${def['group_suffix']}} \ + -B "$@" +} + +# TESTS + + +# CONNECT +# +# Tests that a connection can be made over TCP, the final state +# of the entrypoint and is listening. The authentication used +# isn't tested. +connect() +{ + set +e +o pipefail + mysql ${nodefaults:+--no-defaults} \ + ${def['file']:+--defaults-file=${def['file']}} \ + ${def['extra_file']:+--defaults-extra-file=${def['extra_file']}} \ + ${def['group_suffix']:+--defaults-group-suffix=${def['group_suffix']}} \ + -h localhost --protocol tcp -e 'select 1' 2>&1 \ + | grep -qF "Can't connect" + local ret=${PIPESTATUS[1]} + set -eo pipefail + if (( "$ret" == 0 )); then + # grep Matched "Can't connect" so we fail + return 1 + fi + return 0 +} + +# INNODB_BUFFER_POOL_LOADED +# +# Tests the load of the innodb buffer pool as been complete +# implies innodb_buffer_pool_load_at_startup=1 (default), or if +# manually SET innodb_buffer_pool_load_now=1 +innodb_buffer_pool_loaded() +{ + local s + s=$(_process_sql --skip-column-names -e 'select VARIABLE_VALUE from information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME="Innodb_buffer_pool_load_status"') + if [[ $s =~ 'load completed' ]]; then + return 0 + fi + return 1 +} + +# GALERAONLINE +# +# Tests that the galera node is in the SYNCed state +galeraonline() +{ + local s + s=$(_process_sql --skip-column-names -e 'select VARIABLE_VALUE from information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME="WSREP_LOCAL_STATE"') + # 4 from https://galeracluster.com/library/documentation/node-states.html#node-state-changes + # not https://xkcd.com/221/ + if [[ $s -eq 4 ]]; then + return 0 + fi + return 1 +} + +# REPLICATION +# +# Tests the replication has the required set of functions: +# --replication_all -> Checks all replication sources +# --replication_name=n -> sets the multisource connection name tested +# --replication_io -> IO thread is running +# --replication_sql -> SQL thread is running +# --replication_seconds_behind_master=n -> less than or equal this seconds of delay +# --replication_sql_remaining_delay=n -> less than or equal this seconds of remaining delay +# (ref: https://mariadb.com/kb/en/delayed-replication/) +replication() +{ + # SHOW REPLICA available 10.5+ + # https://github.com/koalaman/shellcheck/issues/2383 + # shellcheck disable=SC2016,SC2026 + _process_sql -e "show ${repl['all']:+all} slave${repl['all']:+s} ${repl['name']:+'${repl['name']}'} status\G" | \ + { + # required for trim of leading space. + shopt -s extglob + # Row header + read -t 5 -r + # read timeout + [ $? -gt 128 ] && return 1 + while IFS=":" read -t 1 -r n v; do + # Trim leading space + n=${n##+([[:space:]])} + # Leading space on all values by the \G format needs to be trimmed. + v=${v:1} + case "$n" in + Slave_IO_Running) + if [ -n "${repl['io']}" ] && [ "$v" = 'No' ]; then + return 1 + fi + ;; + Slave_SQL_Running) + if [ -n "${repl['sql']}" ] && [ "$v" = 'No' ]; then + return 1 + fi + ;; + Seconds_Behind_Master) + # A NULL value is the IO thread not running: + if [ -n "${repl['seconds_behind_master']}" ] && + { [ "$v" = NULL ] || + (( "${repl['seconds_behind_master']}" < "$v" )); }; then + return 1 + fi + ;; + SQL_Remaining_Delay) + # Unlike Seconds_Behind_Master, sql_remaining_delay will hit NULL + # once replication is caught up - https://mariadb.com/kb/en/delayed-replication/ + if [ -n "${repl['sql_remaining_delay']}" ] && + [ "$v" != NULL ] && + (( "${repl['sql_remaining_delay']}" < "$v" )); then + return 1 + fi + ;; + esac + done + # read timeout + [ $? -gt 128 ] && return 1 + return 0 + } + return $? +} + +# mariadbupgrade +# +# Test the lock on the file /var/lib/mysql_upgrade_info +# https://jira.mariadb.org/browse/MDEV-27068 +mariadbupgrade() +{ + local f="$datadir/mysql_upgrade_info" + if [ -r "$f" ]; then + flock --exclusive --nonblock -n 9 9<"$f" + return $? + fi + return 0 +} + + +# MAIN + +if [ $# -eq 0 ]; then + echo "At least one argument required" >&2 + exit 1 +fi + +# Global variables used by tests +declare -A repl +declare -A def +nodefaults= +datadir=/var/lib/mysql + +_repl_param_check() +{ + case "$1" in + seconds_behind_master) ;& + sql_remaining_delay) + if [ -z "${repl['io']}" ]; then + repl['io']=1 + echo "Forcing --replication_io=1, $1 requires IO thread to be running" >&2 + fi + ;; + all) + if [ -n "${repl['name']}" ]; then + unset 'repl[name]' + echo "Option --replication_all incompatible with specied source --replication_name, clearing replication_name" >&2 + fi + ;; + name) + if [ -n "${repl['all']}" ]; then + unset 'repl[all]' + echo "Option --replication_name incompatible with --replication_all, clearing replication_all" >&2 + fi + ;; + esac +} + +_test_exists() { + declare -F "$1" + return $? +} + +# Marks the end of mysql -> mariadb name changes in 10.6+ +#ENDOFSUBSTITUTIONS +while [ $# -gt 0 ]; do + case "$1" in + --su=*) + u="${1#*-}" + shift + exec gosu "${u}" "${BASH_SOURCE[0]}" "$@" + ;; + --su-mysql) + shift + exec gosu mysql "${BASH_SOURCE[0]}" "$@" + ;; + --replication_*=*) + # Change the n to what is between _ and = and make lower case + n=${1#*_} + n=${n%%=*} + n=${n,,*} + # v is after the = + v=${1#*=} + repl[$n]=$v + _repl_param_check "$n" + ;; + --replication_*) + # Without =, look for a non --option next as the value, + # otherwise treat it as an "enable", just equate to 1. + # Clearing option is possible with "--replication_X=" + n=${1#*_} + n=${n,,*} + if [ "${2:0:2}" == '--' ]; then + repl[$n]=1 + else + repl[$n]=$2 + shift + fi + _repl_param_check "$n" + ;; + --datadir=*) + datadir=${1#*=} + ;; + --no-defaults) + unset def + nodefaults=1 + ;; + --defaults-file=*|--defaults-extra-file=*|--defaults-group-suffix=*) + n=${1:11} # length --defaults- + n=${n%%=*} + n=${n//-/_} + # v is after the = + v=${1#*=} + def[$n]=$v + nodefaults= + ;; + --defaults-file|--defaults-extra-file|--defaults-group-suffix) + n=${1:11} # length --defaults- + n=${n//-/_} + if [ "${2:0:2}" == '--' ]; then + def[$n]="" + else + def[$n]=$2 + shift + fi + nodefaults= + ;; + --*) + test=${1#--} + ;; + *) + echo "Unknown healthcheck option $1" >&2 + exit 1 + esac + if [ -n "$test" ]; then + if ! _test_exists "$test" ; then + echo "healthcheck unknown test '$test'" >&2 + exit 1 + elif ! "$test"; then + echo "healthcheck $test failed" >&2 + exit 1 + fi + fi + shift +done diff --git a/10.4/Dockerfile b/10.4/Dockerfile index deb3fde..03c084b 100644 --- a/10.4/Dockerfile +++ b/10.4/Dockerfile @@ -131,6 +131,7 @@ RUN set -ex; \ VOLUME /var/lib/mysql +COPY healthcheck.sh /usr/local/bin/healthcheck.sh COPY docker-entrypoint.sh /usr/local/bin/ RUN ln -s usr/local/bin/docker-entrypoint.sh / # backwards compat ENTRYPOINT ["docker-entrypoint.sh"] diff --git a/10.4/healthcheck.sh b/10.4/healthcheck.sh new file mode 100755 index 0000000..32dfec9 --- /dev/null +++ b/10.4/healthcheck.sh @@ -0,0 +1,306 @@ +#!/bin/bash +# +# Healthcheck script for MariaDB +# +# Runs various tests on the MariaDB server to check its health. Pass the tests +# to run as arguments. If all tests succeed, the server is considered healthy, +# otherwise it's not. +# +# Arguments are processed in strict order. Set replication_* options before +# the --replication option. This allows a different set of replication checks +# on different connections. +# +# --su{=|mysql} is option to run the healthcheck as a different unix user. +# Useful if mysql@localhost user exists with unix socket authentication +# Using this option disregards previous options/tests, so should usually be the +# first option. +# +# Some tests require SQL privileges. +# +# TEST GRANTS REQUIRED +# connect none* +# innodb_buffer_pool_loaded USAGE +# galera_online USAGE +# replication SUPER or REPLICATION_CLIENT or REPLICA MONITOR (10.5+) +# mariadbupgrade none, however unix user permissions on datadir +# +# The SQL user used is the default for the mysql client. This can be the unix user +# if no user(or password) is set in the [mariadb-client] section of a configuration +# file. --defaults-{file,extra-file,group-suffix} can specify a file/configuration +# different from elsewhere. +# +# Note * though denied error message will result in error log without +# any permissions. + +set -eo pipefail + +_process_sql() +{ + mysql ${nodefaults:+--no-defaults} \ + ${def['file']:+--defaults-file=${def['file']}} \ + ${def['extra_file']:+--defaults-extra-file=${def['extra_file']}} \ + ${def['group_suffix']:+--defaults-group-suffix=${def['group_suffix']}} \ + -B "$@" +} + +# TESTS + + +# CONNECT +# +# Tests that a connection can be made over TCP, the final state +# of the entrypoint and is listening. The authentication used +# isn't tested. +connect() +{ + set +e +o pipefail + mysql ${nodefaults:+--no-defaults} \ + ${def['file']:+--defaults-file=${def['file']}} \ + ${def['extra_file']:+--defaults-extra-file=${def['extra_file']}} \ + ${def['group_suffix']:+--defaults-group-suffix=${def['group_suffix']}} \ + -h localhost --protocol tcp -e 'select 1' 2>&1 \ + | grep -qF "Can't connect" + local ret=${PIPESTATUS[1]} + set -eo pipefail + if (( "$ret" == 0 )); then + # grep Matched "Can't connect" so we fail + return 1 + fi + return 0 +} + +# INNODB_BUFFER_POOL_LOADED +# +# Tests the load of the innodb buffer pool as been complete +# implies innodb_buffer_pool_load_at_startup=1 (default), or if +# manually SET innodb_buffer_pool_load_now=1 +innodb_buffer_pool_loaded() +{ + local s + s=$(_process_sql --skip-column-names -e 'select VARIABLE_VALUE from information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME="Innodb_buffer_pool_load_status"') + if [[ $s =~ 'load completed' ]]; then + return 0 + fi + return 1 +} + +# GALERAONLINE +# +# Tests that the galera node is in the SYNCed state +galeraonline() +{ + local s + s=$(_process_sql --skip-column-names -e 'select VARIABLE_VALUE from information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME="WSREP_LOCAL_STATE"') + # 4 from https://galeracluster.com/library/documentation/node-states.html#node-state-changes + # not https://xkcd.com/221/ + if [[ $s -eq 4 ]]; then + return 0 + fi + return 1 +} + +# REPLICATION +# +# Tests the replication has the required set of functions: +# --replication_all -> Checks all replication sources +# --replication_name=n -> sets the multisource connection name tested +# --replication_io -> IO thread is running +# --replication_sql -> SQL thread is running +# --replication_seconds_behind_master=n -> less than or equal this seconds of delay +# --replication_sql_remaining_delay=n -> less than or equal this seconds of remaining delay +# (ref: https://mariadb.com/kb/en/delayed-replication/) +replication() +{ + # SHOW REPLICA available 10.5+ + # https://github.com/koalaman/shellcheck/issues/2383 + # shellcheck disable=SC2016,SC2026 + _process_sql -e "show ${repl['all']:+all} slave${repl['all']:+s} ${repl['name']:+'${repl['name']}'} status\G" | \ + { + # required for trim of leading space. + shopt -s extglob + # Row header + read -t 5 -r + # read timeout + [ $? -gt 128 ] && return 1 + while IFS=":" read -t 1 -r n v; do + # Trim leading space + n=${n##+([[:space:]])} + # Leading space on all values by the \G format needs to be trimmed. + v=${v:1} + case "$n" in + Slave_IO_Running) + if [ -n "${repl['io']}" ] && [ "$v" = 'No' ]; then + return 1 + fi + ;; + Slave_SQL_Running) + if [ -n "${repl['sql']}" ] && [ "$v" = 'No' ]; then + return 1 + fi + ;; + Seconds_Behind_Master) + # A NULL value is the IO thread not running: + if [ -n "${repl['seconds_behind_master']}" ] && + { [ "$v" = NULL ] || + (( "${repl['seconds_behind_master']}" < "$v" )); }; then + return 1 + fi + ;; + SQL_Remaining_Delay) + # Unlike Seconds_Behind_Master, sql_remaining_delay will hit NULL + # once replication is caught up - https://mariadb.com/kb/en/delayed-replication/ + if [ -n "${repl['sql_remaining_delay']}" ] && + [ "$v" != NULL ] && + (( "${repl['sql_remaining_delay']}" < "$v" )); then + return 1 + fi + ;; + esac + done + # read timeout + [ $? -gt 128 ] && return 1 + return 0 + } + return $? +} + +# mariadbupgrade +# +# Test the lock on the file /var/lib/mysql_upgrade_info +# https://jira.mariadb.org/browse/MDEV-27068 +mariadbupgrade() +{ + local f="$datadir/mysql_upgrade_info" + if [ -r "$f" ]; then + flock --exclusive --nonblock -n 9 9<"$f" + return $? + fi + return 0 +} + + +# MAIN + +if [ $# -eq 0 ]; then + echo "At least one argument required" >&2 + exit 1 +fi + +# Global variables used by tests +declare -A repl +declare -A def +nodefaults= +datadir=/var/lib/mysql + +_repl_param_check() +{ + case "$1" in + seconds_behind_master) ;& + sql_remaining_delay) + if [ -z "${repl['io']}" ]; then + repl['io']=1 + echo "Forcing --replication_io=1, $1 requires IO thread to be running" >&2 + fi + ;; + all) + if [ -n "${repl['name']}" ]; then + unset 'repl[name]' + echo "Option --replication_all incompatible with specied source --replication_name, clearing replication_name" >&2 + fi + ;; + name) + if [ -n "${repl['all']}" ]; then + unset 'repl[all]' + echo "Option --replication_name incompatible with --replication_all, clearing replication_all" >&2 + fi + ;; + esac +} + +_test_exists() { + declare -F "$1" + return $? +} + +# Marks the end of mysql -> mariadb name changes in 10.6+ +#ENDOFSUBSTITUTIONS +while [ $# -gt 0 ]; do + case "$1" in + --su=*) + u="${1#*-}" + shift + exec gosu "${u}" "${BASH_SOURCE[0]}" "$@" + ;; + --su-mysql) + shift + exec gosu mysql "${BASH_SOURCE[0]}" "$@" + ;; + --replication_*=*) + # Change the n to what is between _ and = and make lower case + n=${1#*_} + n=${n%%=*} + n=${n,,*} + # v is after the = + v=${1#*=} + repl[$n]=$v + _repl_param_check "$n" + ;; + --replication_*) + # Without =, look for a non --option next as the value, + # otherwise treat it as an "enable", just equate to 1. + # Clearing option is possible with "--replication_X=" + n=${1#*_} + n=${n,,*} + if [ "${2:0:2}" == '--' ]; then + repl[$n]=1 + else + repl[$n]=$2 + shift + fi + _repl_param_check "$n" + ;; + --datadir=*) + datadir=${1#*=} + ;; + --no-defaults) + unset def + nodefaults=1 + ;; + --defaults-file=*|--defaults-extra-file=*|--defaults-group-suffix=*) + n=${1:11} # length --defaults- + n=${n%%=*} + n=${n//-/_} + # v is after the = + v=${1#*=} + def[$n]=$v + nodefaults= + ;; + --defaults-file|--defaults-extra-file|--defaults-group-suffix) + n=${1:11} # length --defaults- + n=${n//-/_} + if [ "${2:0:2}" == '--' ]; then + def[$n]="" + else + def[$n]=$2 + shift + fi + nodefaults= + ;; + --*) + test=${1#--} + ;; + *) + echo "Unknown healthcheck option $1" >&2 + exit 1 + esac + if [ -n "$test" ]; then + if ! _test_exists "$test" ; then + echo "healthcheck unknown test '$test'" >&2 + exit 1 + elif ! "$test"; then + echo "healthcheck $test failed" >&2 + exit 1 + fi + fi + shift +done diff --git a/10.5/Dockerfile b/10.5/Dockerfile index 877bf7a..6add05f 100644 --- a/10.5/Dockerfile +++ b/10.5/Dockerfile @@ -131,6 +131,7 @@ RUN set -ex; \ VOLUME /var/lib/mysql +COPY healthcheck.sh /usr/local/bin/healthcheck.sh COPY docker-entrypoint.sh /usr/local/bin/ ENTRYPOINT ["docker-entrypoint.sh"] diff --git a/10.5/healthcheck.sh b/10.5/healthcheck.sh new file mode 100755 index 0000000..32dfec9 --- /dev/null +++ b/10.5/healthcheck.sh @@ -0,0 +1,306 @@ +#!/bin/bash +# +# Healthcheck script for MariaDB +# +# Runs various tests on the MariaDB server to check its health. Pass the tests +# to run as arguments. If all tests succeed, the server is considered healthy, +# otherwise it's not. +# +# Arguments are processed in strict order. Set replication_* options before +# the --replication option. This allows a different set of replication checks +# on different connections. +# +# --su{=|mysql} is option to run the healthcheck as a different unix user. +# Useful if mysql@localhost user exists with unix socket authentication +# Using this option disregards previous options/tests, so should usually be the +# first option. +# +# Some tests require SQL privileges. +# +# TEST GRANTS REQUIRED +# connect none* +# innodb_buffer_pool_loaded USAGE +# galera_online USAGE +# replication SUPER or REPLICATION_CLIENT or REPLICA MONITOR (10.5+) +# mariadbupgrade none, however unix user permissions on datadir +# +# The SQL user used is the default for the mysql client. This can be the unix user +# if no user(or password) is set in the [mariadb-client] section of a configuration +# file. --defaults-{file,extra-file,group-suffix} can specify a file/configuration +# different from elsewhere. +# +# Note * though denied error message will result in error log without +# any permissions. + +set -eo pipefail + +_process_sql() +{ + mysql ${nodefaults:+--no-defaults} \ + ${def['file']:+--defaults-file=${def['file']}} \ + ${def['extra_file']:+--defaults-extra-file=${def['extra_file']}} \ + ${def['group_suffix']:+--defaults-group-suffix=${def['group_suffix']}} \ + -B "$@" +} + +# TESTS + + +# CONNECT +# +# Tests that a connection can be made over TCP, the final state +# of the entrypoint and is listening. The authentication used +# isn't tested. +connect() +{ + set +e +o pipefail + mysql ${nodefaults:+--no-defaults} \ + ${def['file']:+--defaults-file=${def['file']}} \ + ${def['extra_file']:+--defaults-extra-file=${def['extra_file']}} \ + ${def['group_suffix']:+--defaults-group-suffix=${def['group_suffix']}} \ + -h localhost --protocol tcp -e 'select 1' 2>&1 \ + | grep -qF "Can't connect" + local ret=${PIPESTATUS[1]} + set -eo pipefail + if (( "$ret" == 0 )); then + # grep Matched "Can't connect" so we fail + return 1 + fi + return 0 +} + +# INNODB_BUFFER_POOL_LOADED +# +# Tests the load of the innodb buffer pool as been complete +# implies innodb_buffer_pool_load_at_startup=1 (default), or if +# manually SET innodb_buffer_pool_load_now=1 +innodb_buffer_pool_loaded() +{ + local s + s=$(_process_sql --skip-column-names -e 'select VARIABLE_VALUE from information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME="Innodb_buffer_pool_load_status"') + if [[ $s =~ 'load completed' ]]; then + return 0 + fi + return 1 +} + +# GALERAONLINE +# +# Tests that the galera node is in the SYNCed state +galeraonline() +{ + local s + s=$(_process_sql --skip-column-names -e 'select VARIABLE_VALUE from information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME="WSREP_LOCAL_STATE"') + # 4 from https://galeracluster.com/library/documentation/node-states.html#node-state-changes + # not https://xkcd.com/221/ + if [[ $s -eq 4 ]]; then + return 0 + fi + return 1 +} + +# REPLICATION +# +# Tests the replication has the required set of functions: +# --replication_all -> Checks all replication sources +# --replication_name=n -> sets the multisource connection name tested +# --replication_io -> IO thread is running +# --replication_sql -> SQL thread is running +# --replication_seconds_behind_master=n -> less than or equal this seconds of delay +# --replication_sql_remaining_delay=n -> less than or equal this seconds of remaining delay +# (ref: https://mariadb.com/kb/en/delayed-replication/) +replication() +{ + # SHOW REPLICA available 10.5+ + # https://github.com/koalaman/shellcheck/issues/2383 + # shellcheck disable=SC2016,SC2026 + _process_sql -e "show ${repl['all']:+all} slave${repl['all']:+s} ${repl['name']:+'${repl['name']}'} status\G" | \ + { + # required for trim of leading space. + shopt -s extglob + # Row header + read -t 5 -r + # read timeout + [ $? -gt 128 ] && return 1 + while IFS=":" read -t 1 -r n v; do + # Trim leading space + n=${n##+([[:space:]])} + # Leading space on all values by the \G format needs to be trimmed. + v=${v:1} + case "$n" in + Slave_IO_Running) + if [ -n "${repl['io']}" ] && [ "$v" = 'No' ]; then + return 1 + fi + ;; + Slave_SQL_Running) + if [ -n "${repl['sql']}" ] && [ "$v" = 'No' ]; then + return 1 + fi + ;; + Seconds_Behind_Master) + # A NULL value is the IO thread not running: + if [ -n "${repl['seconds_behind_master']}" ] && + { [ "$v" = NULL ] || + (( "${repl['seconds_behind_master']}" < "$v" )); }; then + return 1 + fi + ;; + SQL_Remaining_Delay) + # Unlike Seconds_Behind_Master, sql_remaining_delay will hit NULL + # once replication is caught up - https://mariadb.com/kb/en/delayed-replication/ + if [ -n "${repl['sql_remaining_delay']}" ] && + [ "$v" != NULL ] && + (( "${repl['sql_remaining_delay']}" < "$v" )); then + return 1 + fi + ;; + esac + done + # read timeout + [ $? -gt 128 ] && return 1 + return 0 + } + return $? +} + +# mariadbupgrade +# +# Test the lock on the file /var/lib/mysql_upgrade_info +# https://jira.mariadb.org/browse/MDEV-27068 +mariadbupgrade() +{ + local f="$datadir/mysql_upgrade_info" + if [ -r "$f" ]; then + flock --exclusive --nonblock -n 9 9<"$f" + return $? + fi + return 0 +} + + +# MAIN + +if [ $# -eq 0 ]; then + echo "At least one argument required" >&2 + exit 1 +fi + +# Global variables used by tests +declare -A repl +declare -A def +nodefaults= +datadir=/var/lib/mysql + +_repl_param_check() +{ + case "$1" in + seconds_behind_master) ;& + sql_remaining_delay) + if [ -z "${repl['io']}" ]; then + repl['io']=1 + echo "Forcing --replication_io=1, $1 requires IO thread to be running" >&2 + fi + ;; + all) + if [ -n "${repl['name']}" ]; then + unset 'repl[name]' + echo "Option --replication_all incompatible with specied source --replication_name, clearing replication_name" >&2 + fi + ;; + name) + if [ -n "${repl['all']}" ]; then + unset 'repl[all]' + echo "Option --replication_name incompatible with --replication_all, clearing replication_all" >&2 + fi + ;; + esac +} + +_test_exists() { + declare -F "$1" + return $? +} + +# Marks the end of mysql -> mariadb name changes in 10.6+ +#ENDOFSUBSTITUTIONS +while [ $# -gt 0 ]; do + case "$1" in + --su=*) + u="${1#*-}" + shift + exec gosu "${u}" "${BASH_SOURCE[0]}" "$@" + ;; + --su-mysql) + shift + exec gosu mysql "${BASH_SOURCE[0]}" "$@" + ;; + --replication_*=*) + # Change the n to what is between _ and = and make lower case + n=${1#*_} + n=${n%%=*} + n=${n,,*} + # v is after the = + v=${1#*=} + repl[$n]=$v + _repl_param_check "$n" + ;; + --replication_*) + # Without =, look for a non --option next as the value, + # otherwise treat it as an "enable", just equate to 1. + # Clearing option is possible with "--replication_X=" + n=${1#*_} + n=${n,,*} + if [ "${2:0:2}" == '--' ]; then + repl[$n]=1 + else + repl[$n]=$2 + shift + fi + _repl_param_check "$n" + ;; + --datadir=*) + datadir=${1#*=} + ;; + --no-defaults) + unset def + nodefaults=1 + ;; + --defaults-file=*|--defaults-extra-file=*|--defaults-group-suffix=*) + n=${1:11} # length --defaults- + n=${n%%=*} + n=${n//-/_} + # v is after the = + v=${1#*=} + def[$n]=$v + nodefaults= + ;; + --defaults-file|--defaults-extra-file|--defaults-group-suffix) + n=${1:11} # length --defaults- + n=${n//-/_} + if [ "${2:0:2}" == '--' ]; then + def[$n]="" + else + def[$n]=$2 + shift + fi + nodefaults= + ;; + --*) + test=${1#--} + ;; + *) + echo "Unknown healthcheck option $1" >&2 + exit 1 + esac + if [ -n "$test" ]; then + if ! _test_exists "$test" ; then + echo "healthcheck unknown test '$test'" >&2 + exit 1 + elif ! "$test"; then + echo "healthcheck $test failed" >&2 + exit 1 + fi + fi + shift +done diff --git a/10.6/Dockerfile b/10.6/Dockerfile index 3ddc538..2e8a00e 100644 --- a/10.6/Dockerfile +++ b/10.6/Dockerfile @@ -131,6 +131,7 @@ RUN set -ex; \ VOLUME /var/lib/mysql +COPY healthcheck.sh /usr/local/bin/healthcheck.sh COPY docker-entrypoint.sh /usr/local/bin/ ENTRYPOINT ["docker-entrypoint.sh"] diff --git a/10.6/healthcheck.sh b/10.6/healthcheck.sh new file mode 100755 index 0000000..352f1cd --- /dev/null +++ b/10.6/healthcheck.sh @@ -0,0 +1,306 @@ +#!/bin/bash +# +# Healthcheck script for MariaDB +# +# Runs various tests on the MariaDB server to check its health. Pass the tests +# to run as arguments. If all tests succeed, the server is considered healthy, +# otherwise it's not. +# +# Arguments are processed in strict order. Set replication_* options before +# the --replication option. This allows a different set of replication checks +# on different connections. +# +# --su{=|mariadb} is option to run the healthcheck as a different unix user. +# Useful if mariadb@localhost user exists with unix socket authentication +# Using this option disregards previous options/tests, so should usually be the +# first option. +# +# Some tests require SQL privileges. +# +# TEST GRANTS REQUIRED +# connect none* +# innodb_buffer_pool_loaded USAGE +# galera_online USAGE +# replication SUPER or REPLICATION_CLIENT or REPLICA MONITOR (10.5+) +# mariadbupgrade none, however unix user permissions on datadir +# +# The SQL user used is the default for the mariadb client. This can be the unix user +# if no user(or password) is set in the [mariadb-client] section of a configuration +# file. --defaults-{file,extra-file,group-suffix} can specify a file/configuration +# different from elsewhere. +# +# Note * though denied error message will result in error log without +# any permissions. + +set -eo pipefail + +_process_sql() +{ + mariadb ${nodefaults:+--no-defaults} \ + ${def['file']:+--defaults-file=${def['file']}} \ + ${def['extra_file']:+--defaults-extra-file=${def['extra_file']}} \ + ${def['group_suffix']:+--defaults-group-suffix=${def['group_suffix']}} \ + -B "$@" +} + +# TESTS + + +# CONNECT +# +# Tests that a connection can be made over TCP, the final state +# of the entrypoint and is listening. The authentication used +# isn't tested. +connect() +{ + set +e +o pipefail + mariadb ${nodefaults:+--no-defaults} \ + ${def['file']:+--defaults-file=${def['file']}} \ + ${def['extra_file']:+--defaults-extra-file=${def['extra_file']}} \ + ${def['group_suffix']:+--defaults-group-suffix=${def['group_suffix']}} \ + -h localhost --protocol tcp -e 'select 1' 2>&1 \ + | grep -qF "Can't connect" + local ret=${PIPESTATUS[1]} + set -eo pipefail + if (( "$ret" == 0 )); then + # grep Matched "Can't connect" so we fail + return 1 + fi + return 0 +} + +# INNODB_BUFFER_POOL_LOADED +# +# Tests the load of the innodb buffer pool as been complete +# implies innodb_buffer_pool_load_at_startup=1 (default), or if +# manually SET innodb_buffer_pool_load_now=1 +innodb_buffer_pool_loaded() +{ + local s + s=$(_process_sql --skip-column-names -e 'select VARIABLE_VALUE from information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME="Innodb_buffer_pool_load_status"') + if [[ $s =~ 'load completed' ]]; then + return 0 + fi + return 1 +} + +# GALERAONLINE +# +# Tests that the galera node is in the SYNCed state +galeraonline() +{ + local s + s=$(_process_sql --skip-column-names -e 'select VARIABLE_VALUE from information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME="WSREP_LOCAL_STATE"') + # 4 from https://galeracluster.com/library/documentation/node-states.html#node-state-changes + # not https://xkcd.com/221/ + if [[ $s -eq 4 ]]; then + return 0 + fi + return 1 +} + +# REPLICATION +# +# Tests the replication has the required set of functions: +# --replication_all -> Checks all replication sources +# --replication_name=n -> sets the multisource connection name tested +# --replication_io -> IO thread is running +# --replication_sql -> SQL thread is running +# --replication_seconds_behind_master=n -> less than or equal this seconds of delay +# --replication_sql_remaining_delay=n -> less than or equal this seconds of remaining delay +# (ref: https://mariadb.com/kb/en/delayed-replication/) +replication() +{ + # SHOW REPLICA available 10.5+ + # https://github.com/koalaman/shellcheck/issues/2383 + # shellcheck disable=SC2016,SC2026 + _process_sql -e "show ${repl['all']:+all} slave${repl['all']:+s} ${repl['name']:+'${repl['name']}'} status\G" | \ + { + # required for trim of leading space. + shopt -s extglob + # Row header + read -t 5 -r + # read timeout + [ $? -gt 128 ] && return 1 + while IFS=":" read -t 1 -r n v; do + # Trim leading space + n=${n##+([[:space:]])} + # Leading space on all values by the \G format needs to be trimmed. + v=${v:1} + case "$n" in + Slave_IO_Running) + if [ -n "${repl['io']}" ] && [ "$v" = 'No' ]; then + return 1 + fi + ;; + Slave_SQL_Running) + if [ -n "${repl['sql']}" ] && [ "$v" = 'No' ]; then + return 1 + fi + ;; + Seconds_Behind_Master) + # A NULL value is the IO thread not running: + if [ -n "${repl['seconds_behind_master']}" ] && + { [ "$v" = NULL ] || + (( "${repl['seconds_behind_master']}" < "$v" )); }; then + return 1 + fi + ;; + SQL_Remaining_Delay) + # Unlike Seconds_Behind_Master, sql_remaining_delay will hit NULL + # once replication is caught up - https://mariadb.com/kb/en/delayed-replication/ + if [ -n "${repl['sql_remaining_delay']}" ] && + [ "$v" != NULL ] && + (( "${repl['sql_remaining_delay']}" < "$v" )); then + return 1 + fi + ;; + esac + done + # read timeout + [ $? -gt 128 ] && return 1 + return 0 + } + return $? +} + +# mariadbupgrade +# +# Test the lock on the file /var/lib/mysql_upgrade_info +# https://jira.mariadb.org/browse/MDEV-27068 +mariadbupgrade() +{ + local f="$datadir/mysql_upgrade_info" + if [ -r "$f" ]; then + flock --exclusive --nonblock -n 9 9<"$f" + return $? + fi + return 0 +} + + +# MAIN + +if [ $# -eq 0 ]; then + echo "At least one argument required" >&2 + exit 1 +fi + +# Global variables used by tests +declare -A repl +declare -A def +nodefaults= +datadir=/var/lib/mariadb + +_repl_param_check() +{ + case "$1" in + seconds_behind_master) ;& + sql_remaining_delay) + if [ -z "${repl['io']}" ]; then + repl['io']=1 + echo "Forcing --replication_io=1, $1 requires IO thread to be running" >&2 + fi + ;; + all) + if [ -n "${repl['name']}" ]; then + unset 'repl[name]' + echo "Option --replication_all incompatible with specied source --replication_name, clearing replication_name" >&2 + fi + ;; + name) + if [ -n "${repl['all']}" ]; then + unset 'repl[all]' + echo "Option --replication_name incompatible with --replication_all, clearing replication_all" >&2 + fi + ;; + esac +} + +_test_exists() { + declare -F "$1" + return $? +} + +# Marks the end of mariadb -> mariadb name changes in 10.6+ +#ENDOFSUBSTITUTIONS +while [ $# -gt 0 ]; do + case "$1" in + --su=*) + u="${1#*-}" + shift + exec gosu "${u}" "${BASH_SOURCE[0]}" "$@" + ;; + --su-mariadb) + shift + exec gosu mariadb "${BASH_SOURCE[0]}" "$@" + ;; + --replication_*=*) + # Change the n to what is between _ and = and make lower case + n=${1#*_} + n=${n%%=*} + n=${n,,*} + # v is after the = + v=${1#*=} + repl[$n]=$v + _repl_param_check "$n" + ;; + --replication_*) + # Without =, look for a non --option next as the value, + # otherwise treat it as an "enable", just equate to 1. + # Clearing option is possible with "--replication_X=" + n=${1#*_} + n=${n,,*} + if [ "${2:0:2}" == '--' ]; then + repl[$n]=1 + else + repl[$n]=$2 + shift + fi + _repl_param_check "$n" + ;; + --datadir=*) + datadir=${1#*=} + ;; + --no-defaults) + unset def + nodefaults=1 + ;; + --defaults-file=*|--defaults-extra-file=*|--defaults-group-suffix=*) + n=${1:11} # length --defaults- + n=${n%%=*} + n=${n//-/_} + # v is after the = + v=${1#*=} + def[$n]=$v + nodefaults= + ;; + --defaults-file|--defaults-extra-file|--defaults-group-suffix) + n=${1:11} # length --defaults- + n=${n//-/_} + if [ "${2:0:2}" == '--' ]; then + def[$n]="" + else + def[$n]=$2 + shift + fi + nodefaults= + ;; + --*) + test=${1#--} + ;; + *) + echo "Unknown healthcheck option $1" >&2 + exit 1 + esac + if [ -n "$test" ]; then + if ! _test_exists "$test" ; then + echo "healthcheck unknown test '$test'" >&2 + exit 1 + elif ! "$test"; then + echo "healthcheck $test failed" >&2 + exit 1 + fi + fi + shift +done diff --git a/10.7/Dockerfile b/10.7/Dockerfile index e545755..63200a0 100644 --- a/10.7/Dockerfile +++ b/10.7/Dockerfile @@ -131,6 +131,7 @@ RUN set -ex; \ VOLUME /var/lib/mysql +COPY healthcheck.sh /usr/local/bin/healthcheck.sh COPY docker-entrypoint.sh /usr/local/bin/ ENTRYPOINT ["docker-entrypoint.sh"] diff --git a/10.7/healthcheck.sh b/10.7/healthcheck.sh new file mode 100755 index 0000000..352f1cd --- /dev/null +++ b/10.7/healthcheck.sh @@ -0,0 +1,306 @@ +#!/bin/bash +# +# Healthcheck script for MariaDB +# +# Runs various tests on the MariaDB server to check its health. Pass the tests +# to run as arguments. If all tests succeed, the server is considered healthy, +# otherwise it's not. +# +# Arguments are processed in strict order. Set replication_* options before +# the --replication option. This allows a different set of replication checks +# on different connections. +# +# --su{=|mariadb} is option to run the healthcheck as a different unix user. +# Useful if mariadb@localhost user exists with unix socket authentication +# Using this option disregards previous options/tests, so should usually be the +# first option. +# +# Some tests require SQL privileges. +# +# TEST GRANTS REQUIRED +# connect none* +# innodb_buffer_pool_loaded USAGE +# galera_online USAGE +# replication SUPER or REPLICATION_CLIENT or REPLICA MONITOR (10.5+) +# mariadbupgrade none, however unix user permissions on datadir +# +# The SQL user used is the default for the mariadb client. This can be the unix user +# if no user(or password) is set in the [mariadb-client] section of a configuration +# file. --defaults-{file,extra-file,group-suffix} can specify a file/configuration +# different from elsewhere. +# +# Note * though denied error message will result in error log without +# any permissions. + +set -eo pipefail + +_process_sql() +{ + mariadb ${nodefaults:+--no-defaults} \ + ${def['file']:+--defaults-file=${def['file']}} \ + ${def['extra_file']:+--defaults-extra-file=${def['extra_file']}} \ + ${def['group_suffix']:+--defaults-group-suffix=${def['group_suffix']}} \ + -B "$@" +} + +# TESTS + + +# CONNECT +# +# Tests that a connection can be made over TCP, the final state +# of the entrypoint and is listening. The authentication used +# isn't tested. +connect() +{ + set +e +o pipefail + mariadb ${nodefaults:+--no-defaults} \ + ${def['file']:+--defaults-file=${def['file']}} \ + ${def['extra_file']:+--defaults-extra-file=${def['extra_file']}} \ + ${def['group_suffix']:+--defaults-group-suffix=${def['group_suffix']}} \ + -h localhost --protocol tcp -e 'select 1' 2>&1 \ + | grep -qF "Can't connect" + local ret=${PIPESTATUS[1]} + set -eo pipefail + if (( "$ret" == 0 )); then + # grep Matched "Can't connect" so we fail + return 1 + fi + return 0 +} + +# INNODB_BUFFER_POOL_LOADED +# +# Tests the load of the innodb buffer pool as been complete +# implies innodb_buffer_pool_load_at_startup=1 (default), or if +# manually SET innodb_buffer_pool_load_now=1 +innodb_buffer_pool_loaded() +{ + local s + s=$(_process_sql --skip-column-names -e 'select VARIABLE_VALUE from information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME="Innodb_buffer_pool_load_status"') + if [[ $s =~ 'load completed' ]]; then + return 0 + fi + return 1 +} + +# GALERAONLINE +# +# Tests that the galera node is in the SYNCed state +galeraonline() +{ + local s + s=$(_process_sql --skip-column-names -e 'select VARIABLE_VALUE from information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME="WSREP_LOCAL_STATE"') + # 4 from https://galeracluster.com/library/documentation/node-states.html#node-state-changes + # not https://xkcd.com/221/ + if [[ $s -eq 4 ]]; then + return 0 + fi + return 1 +} + +# REPLICATION +# +# Tests the replication has the required set of functions: +# --replication_all -> Checks all replication sources +# --replication_name=n -> sets the multisource connection name tested +# --replication_io -> IO thread is running +# --replication_sql -> SQL thread is running +# --replication_seconds_behind_master=n -> less than or equal this seconds of delay +# --replication_sql_remaining_delay=n -> less than or equal this seconds of remaining delay +# (ref: https://mariadb.com/kb/en/delayed-replication/) +replication() +{ + # SHOW REPLICA available 10.5+ + # https://github.com/koalaman/shellcheck/issues/2383 + # shellcheck disable=SC2016,SC2026 + _process_sql -e "show ${repl['all']:+all} slave${repl['all']:+s} ${repl['name']:+'${repl['name']}'} status\G" | \ + { + # required for trim of leading space. + shopt -s extglob + # Row header + read -t 5 -r + # read timeout + [ $? -gt 128 ] && return 1 + while IFS=":" read -t 1 -r n v; do + # Trim leading space + n=${n##+([[:space:]])} + # Leading space on all values by the \G format needs to be trimmed. + v=${v:1} + case "$n" in + Slave_IO_Running) + if [ -n "${repl['io']}" ] && [ "$v" = 'No' ]; then + return 1 + fi + ;; + Slave_SQL_Running) + if [ -n "${repl['sql']}" ] && [ "$v" = 'No' ]; then + return 1 + fi + ;; + Seconds_Behind_Master) + # A NULL value is the IO thread not running: + if [ -n "${repl['seconds_behind_master']}" ] && + { [ "$v" = NULL ] || + (( "${repl['seconds_behind_master']}" < "$v" )); }; then + return 1 + fi + ;; + SQL_Remaining_Delay) + # Unlike Seconds_Behind_Master, sql_remaining_delay will hit NULL + # once replication is caught up - https://mariadb.com/kb/en/delayed-replication/ + if [ -n "${repl['sql_remaining_delay']}" ] && + [ "$v" != NULL ] && + (( "${repl['sql_remaining_delay']}" < "$v" )); then + return 1 + fi + ;; + esac + done + # read timeout + [ $? -gt 128 ] && return 1 + return 0 + } + return $? +} + +# mariadbupgrade +# +# Test the lock on the file /var/lib/mysql_upgrade_info +# https://jira.mariadb.org/browse/MDEV-27068 +mariadbupgrade() +{ + local f="$datadir/mysql_upgrade_info" + if [ -r "$f" ]; then + flock --exclusive --nonblock -n 9 9<"$f" + return $? + fi + return 0 +} + + +# MAIN + +if [ $# -eq 0 ]; then + echo "At least one argument required" >&2 + exit 1 +fi + +# Global variables used by tests +declare -A repl +declare -A def +nodefaults= +datadir=/var/lib/mariadb + +_repl_param_check() +{ + case "$1" in + seconds_behind_master) ;& + sql_remaining_delay) + if [ -z "${repl['io']}" ]; then + repl['io']=1 + echo "Forcing --replication_io=1, $1 requires IO thread to be running" >&2 + fi + ;; + all) + if [ -n "${repl['name']}" ]; then + unset 'repl[name]' + echo "Option --replication_all incompatible with specied source --replication_name, clearing replication_name" >&2 + fi + ;; + name) + if [ -n "${repl['all']}" ]; then + unset 'repl[all]' + echo "Option --replication_name incompatible with --replication_all, clearing replication_all" >&2 + fi + ;; + esac +} + +_test_exists() { + declare -F "$1" + return $? +} + +# Marks the end of mariadb -> mariadb name changes in 10.6+ +#ENDOFSUBSTITUTIONS +while [ $# -gt 0 ]; do + case "$1" in + --su=*) + u="${1#*-}" + shift + exec gosu "${u}" "${BASH_SOURCE[0]}" "$@" + ;; + --su-mariadb) + shift + exec gosu mariadb "${BASH_SOURCE[0]}" "$@" + ;; + --replication_*=*) + # Change the n to what is between _ and = and make lower case + n=${1#*_} + n=${n%%=*} + n=${n,,*} + # v is after the = + v=${1#*=} + repl[$n]=$v + _repl_param_check "$n" + ;; + --replication_*) + # Without =, look for a non --option next as the value, + # otherwise treat it as an "enable", just equate to 1. + # Clearing option is possible with "--replication_X=" + n=${1#*_} + n=${n,,*} + if [ "${2:0:2}" == '--' ]; then + repl[$n]=1 + else + repl[$n]=$2 + shift + fi + _repl_param_check "$n" + ;; + --datadir=*) + datadir=${1#*=} + ;; + --no-defaults) + unset def + nodefaults=1 + ;; + --defaults-file=*|--defaults-extra-file=*|--defaults-group-suffix=*) + n=${1:11} # length --defaults- + n=${n%%=*} + n=${n//-/_} + # v is after the = + v=${1#*=} + def[$n]=$v + nodefaults= + ;; + --defaults-file|--defaults-extra-file|--defaults-group-suffix) + n=${1:11} # length --defaults- + n=${n//-/_} + if [ "${2:0:2}" == '--' ]; then + def[$n]="" + else + def[$n]=$2 + shift + fi + nodefaults= + ;; + --*) + test=${1#--} + ;; + *) + echo "Unknown healthcheck option $1" >&2 + exit 1 + esac + if [ -n "$test" ]; then + if ! _test_exists "$test" ; then + echo "healthcheck unknown test '$test'" >&2 + exit 1 + elif ! "$test"; then + echo "healthcheck $test failed" >&2 + exit 1 + fi + fi + shift +done diff --git a/10.8/Dockerfile b/10.8/Dockerfile index 3e9167c..becb9ea 100644 --- a/10.8/Dockerfile +++ b/10.8/Dockerfile @@ -131,6 +131,7 @@ RUN set -ex; \ VOLUME /var/lib/mysql +COPY healthcheck.sh /usr/local/bin/healthcheck.sh COPY docker-entrypoint.sh /usr/local/bin/ ENTRYPOINT ["docker-entrypoint.sh"] diff --git a/10.8/healthcheck.sh b/10.8/healthcheck.sh new file mode 100755 index 0000000..352f1cd --- /dev/null +++ b/10.8/healthcheck.sh @@ -0,0 +1,306 @@ +#!/bin/bash +# +# Healthcheck script for MariaDB +# +# Runs various tests on the MariaDB server to check its health. Pass the tests +# to run as arguments. If all tests succeed, the server is considered healthy, +# otherwise it's not. +# +# Arguments are processed in strict order. Set replication_* options before +# the --replication option. This allows a different set of replication checks +# on different connections. +# +# --su{=|mariadb} is option to run the healthcheck as a different unix user. +# Useful if mariadb@localhost user exists with unix socket authentication +# Using this option disregards previous options/tests, so should usually be the +# first option. +# +# Some tests require SQL privileges. +# +# TEST GRANTS REQUIRED +# connect none* +# innodb_buffer_pool_loaded USAGE +# galera_online USAGE +# replication SUPER or REPLICATION_CLIENT or REPLICA MONITOR (10.5+) +# mariadbupgrade none, however unix user permissions on datadir +# +# The SQL user used is the default for the mariadb client. This can be the unix user +# if no user(or password) is set in the [mariadb-client] section of a configuration +# file. --defaults-{file,extra-file,group-suffix} can specify a file/configuration +# different from elsewhere. +# +# Note * though denied error message will result in error log without +# any permissions. + +set -eo pipefail + +_process_sql() +{ + mariadb ${nodefaults:+--no-defaults} \ + ${def['file']:+--defaults-file=${def['file']}} \ + ${def['extra_file']:+--defaults-extra-file=${def['extra_file']}} \ + ${def['group_suffix']:+--defaults-group-suffix=${def['group_suffix']}} \ + -B "$@" +} + +# TESTS + + +# CONNECT +# +# Tests that a connection can be made over TCP, the final state +# of the entrypoint and is listening. The authentication used +# isn't tested. +connect() +{ + set +e +o pipefail + mariadb ${nodefaults:+--no-defaults} \ + ${def['file']:+--defaults-file=${def['file']}} \ + ${def['extra_file']:+--defaults-extra-file=${def['extra_file']}} \ + ${def['group_suffix']:+--defaults-group-suffix=${def['group_suffix']}} \ + -h localhost --protocol tcp -e 'select 1' 2>&1 \ + | grep -qF "Can't connect" + local ret=${PIPESTATUS[1]} + set -eo pipefail + if (( "$ret" == 0 )); then + # grep Matched "Can't connect" so we fail + return 1 + fi + return 0 +} + +# INNODB_BUFFER_POOL_LOADED +# +# Tests the load of the innodb buffer pool as been complete +# implies innodb_buffer_pool_load_at_startup=1 (default), or if +# manually SET innodb_buffer_pool_load_now=1 +innodb_buffer_pool_loaded() +{ + local s + s=$(_process_sql --skip-column-names -e 'select VARIABLE_VALUE from information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME="Innodb_buffer_pool_load_status"') + if [[ $s =~ 'load completed' ]]; then + return 0 + fi + return 1 +} + +# GALERAONLINE +# +# Tests that the galera node is in the SYNCed state +galeraonline() +{ + local s + s=$(_process_sql --skip-column-names -e 'select VARIABLE_VALUE from information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME="WSREP_LOCAL_STATE"') + # 4 from https://galeracluster.com/library/documentation/node-states.html#node-state-changes + # not https://xkcd.com/221/ + if [[ $s -eq 4 ]]; then + return 0 + fi + return 1 +} + +# REPLICATION +# +# Tests the replication has the required set of functions: +# --replication_all -> Checks all replication sources +# --replication_name=n -> sets the multisource connection name tested +# --replication_io -> IO thread is running +# --replication_sql -> SQL thread is running +# --replication_seconds_behind_master=n -> less than or equal this seconds of delay +# --replication_sql_remaining_delay=n -> less than or equal this seconds of remaining delay +# (ref: https://mariadb.com/kb/en/delayed-replication/) +replication() +{ + # SHOW REPLICA available 10.5+ + # https://github.com/koalaman/shellcheck/issues/2383 + # shellcheck disable=SC2016,SC2026 + _process_sql -e "show ${repl['all']:+all} slave${repl['all']:+s} ${repl['name']:+'${repl['name']}'} status\G" | \ + { + # required for trim of leading space. + shopt -s extglob + # Row header + read -t 5 -r + # read timeout + [ $? -gt 128 ] && return 1 + while IFS=":" read -t 1 -r n v; do + # Trim leading space + n=${n##+([[:space:]])} + # Leading space on all values by the \G format needs to be trimmed. + v=${v:1} + case "$n" in + Slave_IO_Running) + if [ -n "${repl['io']}" ] && [ "$v" = 'No' ]; then + return 1 + fi + ;; + Slave_SQL_Running) + if [ -n "${repl['sql']}" ] && [ "$v" = 'No' ]; then + return 1 + fi + ;; + Seconds_Behind_Master) + # A NULL value is the IO thread not running: + if [ -n "${repl['seconds_behind_master']}" ] && + { [ "$v" = NULL ] || + (( "${repl['seconds_behind_master']}" < "$v" )); }; then + return 1 + fi + ;; + SQL_Remaining_Delay) + # Unlike Seconds_Behind_Master, sql_remaining_delay will hit NULL + # once replication is caught up - https://mariadb.com/kb/en/delayed-replication/ + if [ -n "${repl['sql_remaining_delay']}" ] && + [ "$v" != NULL ] && + (( "${repl['sql_remaining_delay']}" < "$v" )); then + return 1 + fi + ;; + esac + done + # read timeout + [ $? -gt 128 ] && return 1 + return 0 + } + return $? +} + +# mariadbupgrade +# +# Test the lock on the file /var/lib/mysql_upgrade_info +# https://jira.mariadb.org/browse/MDEV-27068 +mariadbupgrade() +{ + local f="$datadir/mysql_upgrade_info" + if [ -r "$f" ]; then + flock --exclusive --nonblock -n 9 9<"$f" + return $? + fi + return 0 +} + + +# MAIN + +if [ $# -eq 0 ]; then + echo "At least one argument required" >&2 + exit 1 +fi + +# Global variables used by tests +declare -A repl +declare -A def +nodefaults= +datadir=/var/lib/mariadb + +_repl_param_check() +{ + case "$1" in + seconds_behind_master) ;& + sql_remaining_delay) + if [ -z "${repl['io']}" ]; then + repl['io']=1 + echo "Forcing --replication_io=1, $1 requires IO thread to be running" >&2 + fi + ;; + all) + if [ -n "${repl['name']}" ]; then + unset 'repl[name]' + echo "Option --replication_all incompatible with specied source --replication_name, clearing replication_name" >&2 + fi + ;; + name) + if [ -n "${repl['all']}" ]; then + unset 'repl[all]' + echo "Option --replication_name incompatible with --replication_all, clearing replication_all" >&2 + fi + ;; + esac +} + +_test_exists() { + declare -F "$1" + return $? +} + +# Marks the end of mariadb -> mariadb name changes in 10.6+ +#ENDOFSUBSTITUTIONS +while [ $# -gt 0 ]; do + case "$1" in + --su=*) + u="${1#*-}" + shift + exec gosu "${u}" "${BASH_SOURCE[0]}" "$@" + ;; + --su-mariadb) + shift + exec gosu mariadb "${BASH_SOURCE[0]}" "$@" + ;; + --replication_*=*) + # Change the n to what is between _ and = and make lower case + n=${1#*_} + n=${n%%=*} + n=${n,,*} + # v is after the = + v=${1#*=} + repl[$n]=$v + _repl_param_check "$n" + ;; + --replication_*) + # Without =, look for a non --option next as the value, + # otherwise treat it as an "enable", just equate to 1. + # Clearing option is possible with "--replication_X=" + n=${1#*_} + n=${n,,*} + if [ "${2:0:2}" == '--' ]; then + repl[$n]=1 + else + repl[$n]=$2 + shift + fi + _repl_param_check "$n" + ;; + --datadir=*) + datadir=${1#*=} + ;; + --no-defaults) + unset def + nodefaults=1 + ;; + --defaults-file=*|--defaults-extra-file=*|--defaults-group-suffix=*) + n=${1:11} # length --defaults- + n=${n%%=*} + n=${n//-/_} + # v is after the = + v=${1#*=} + def[$n]=$v + nodefaults= + ;; + --defaults-file|--defaults-extra-file|--defaults-group-suffix) + n=${1:11} # length --defaults- + n=${n//-/_} + if [ "${2:0:2}" == '--' ]; then + def[$n]="" + else + def[$n]=$2 + shift + fi + nodefaults= + ;; + --*) + test=${1#--} + ;; + *) + echo "Unknown healthcheck option $1" >&2 + exit 1 + esac + if [ -n "$test" ]; then + if ! _test_exists "$test" ; then + echo "healthcheck unknown test '$test'" >&2 + exit 1 + elif ! "$test"; then + echo "healthcheck $test failed" >&2 + exit 1 + fi + fi + shift +done diff --git a/Dockerfile.template b/Dockerfile.template index 7c4aab6..dd20f99 100644 --- a/Dockerfile.template +++ b/Dockerfile.template @@ -131,6 +131,7 @@ RUN set -ex; \ VOLUME /var/lib/mysql +COPY healthcheck.sh /usr/local/bin/healthcheck.sh COPY docker-entrypoint.sh /usr/local/bin/ RUN ln -s usr/local/bin/docker-entrypoint.sh / # backwards compat ENTRYPOINT ["docker-entrypoint.sh"] diff --git a/healthcheck.sh b/healthcheck.sh new file mode 100755 index 0000000..32dfec9 --- /dev/null +++ b/healthcheck.sh @@ -0,0 +1,306 @@ +#!/bin/bash +# +# Healthcheck script for MariaDB +# +# Runs various tests on the MariaDB server to check its health. Pass the tests +# to run as arguments. If all tests succeed, the server is considered healthy, +# otherwise it's not. +# +# Arguments are processed in strict order. Set replication_* options before +# the --replication option. This allows a different set of replication checks +# on different connections. +# +# --su{=|mysql} is option to run the healthcheck as a different unix user. +# Useful if mysql@localhost user exists with unix socket authentication +# Using this option disregards previous options/tests, so should usually be the +# first option. +# +# Some tests require SQL privileges. +# +# TEST GRANTS REQUIRED +# connect none* +# innodb_buffer_pool_loaded USAGE +# galera_online USAGE +# replication SUPER or REPLICATION_CLIENT or REPLICA MONITOR (10.5+) +# mariadbupgrade none, however unix user permissions on datadir +# +# The SQL user used is the default for the mysql client. This can be the unix user +# if no user(or password) is set in the [mariadb-client] section of a configuration +# file. --defaults-{file,extra-file,group-suffix} can specify a file/configuration +# different from elsewhere. +# +# Note * though denied error message will result in error log without +# any permissions. + +set -eo pipefail + +_process_sql() +{ + mysql ${nodefaults:+--no-defaults} \ + ${def['file']:+--defaults-file=${def['file']}} \ + ${def['extra_file']:+--defaults-extra-file=${def['extra_file']}} \ + ${def['group_suffix']:+--defaults-group-suffix=${def['group_suffix']}} \ + -B "$@" +} + +# TESTS + + +# CONNECT +# +# Tests that a connection can be made over TCP, the final state +# of the entrypoint and is listening. The authentication used +# isn't tested. +connect() +{ + set +e +o pipefail + mysql ${nodefaults:+--no-defaults} \ + ${def['file']:+--defaults-file=${def['file']}} \ + ${def['extra_file']:+--defaults-extra-file=${def['extra_file']}} \ + ${def['group_suffix']:+--defaults-group-suffix=${def['group_suffix']}} \ + -h localhost --protocol tcp -e 'select 1' 2>&1 \ + | grep -qF "Can't connect" + local ret=${PIPESTATUS[1]} + set -eo pipefail + if (( "$ret" == 0 )); then + # grep Matched "Can't connect" so we fail + return 1 + fi + return 0 +} + +# INNODB_BUFFER_POOL_LOADED +# +# Tests the load of the innodb buffer pool as been complete +# implies innodb_buffer_pool_load_at_startup=1 (default), or if +# manually SET innodb_buffer_pool_load_now=1 +innodb_buffer_pool_loaded() +{ + local s + s=$(_process_sql --skip-column-names -e 'select VARIABLE_VALUE from information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME="Innodb_buffer_pool_load_status"') + if [[ $s =~ 'load completed' ]]; then + return 0 + fi + return 1 +} + +# GALERAONLINE +# +# Tests that the galera node is in the SYNCed state +galeraonline() +{ + local s + s=$(_process_sql --skip-column-names -e 'select VARIABLE_VALUE from information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME="WSREP_LOCAL_STATE"') + # 4 from https://galeracluster.com/library/documentation/node-states.html#node-state-changes + # not https://xkcd.com/221/ + if [[ $s -eq 4 ]]; then + return 0 + fi + return 1 +} + +# REPLICATION +# +# Tests the replication has the required set of functions: +# --replication_all -> Checks all replication sources +# --replication_name=n -> sets the multisource connection name tested +# --replication_io -> IO thread is running +# --replication_sql -> SQL thread is running +# --replication_seconds_behind_master=n -> less than or equal this seconds of delay +# --replication_sql_remaining_delay=n -> less than or equal this seconds of remaining delay +# (ref: https://mariadb.com/kb/en/delayed-replication/) +replication() +{ + # SHOW REPLICA available 10.5+ + # https://github.com/koalaman/shellcheck/issues/2383 + # shellcheck disable=SC2016,SC2026 + _process_sql -e "show ${repl['all']:+all} slave${repl['all']:+s} ${repl['name']:+'${repl['name']}'} status\G" | \ + { + # required for trim of leading space. + shopt -s extglob + # Row header + read -t 5 -r + # read timeout + [ $? -gt 128 ] && return 1 + while IFS=":" read -t 1 -r n v; do + # Trim leading space + n=${n##+([[:space:]])} + # Leading space on all values by the \G format needs to be trimmed. + v=${v:1} + case "$n" in + Slave_IO_Running) + if [ -n "${repl['io']}" ] && [ "$v" = 'No' ]; then + return 1 + fi + ;; + Slave_SQL_Running) + if [ -n "${repl['sql']}" ] && [ "$v" = 'No' ]; then + return 1 + fi + ;; + Seconds_Behind_Master) + # A NULL value is the IO thread not running: + if [ -n "${repl['seconds_behind_master']}" ] && + { [ "$v" = NULL ] || + (( "${repl['seconds_behind_master']}" < "$v" )); }; then + return 1 + fi + ;; + SQL_Remaining_Delay) + # Unlike Seconds_Behind_Master, sql_remaining_delay will hit NULL + # once replication is caught up - https://mariadb.com/kb/en/delayed-replication/ + if [ -n "${repl['sql_remaining_delay']}" ] && + [ "$v" != NULL ] && + (( "${repl['sql_remaining_delay']}" < "$v" )); then + return 1 + fi + ;; + esac + done + # read timeout + [ $? -gt 128 ] && return 1 + return 0 + } + return $? +} + +# mariadbupgrade +# +# Test the lock on the file /var/lib/mysql_upgrade_info +# https://jira.mariadb.org/browse/MDEV-27068 +mariadbupgrade() +{ + local f="$datadir/mysql_upgrade_info" + if [ -r "$f" ]; then + flock --exclusive --nonblock -n 9 9<"$f" + return $? + fi + return 0 +} + + +# MAIN + +if [ $# -eq 0 ]; then + echo "At least one argument required" >&2 + exit 1 +fi + +# Global variables used by tests +declare -A repl +declare -A def +nodefaults= +datadir=/var/lib/mysql + +_repl_param_check() +{ + case "$1" in + seconds_behind_master) ;& + sql_remaining_delay) + if [ -z "${repl['io']}" ]; then + repl['io']=1 + echo "Forcing --replication_io=1, $1 requires IO thread to be running" >&2 + fi + ;; + all) + if [ -n "${repl['name']}" ]; then + unset 'repl[name]' + echo "Option --replication_all incompatible with specied source --replication_name, clearing replication_name" >&2 + fi + ;; + name) + if [ -n "${repl['all']}" ]; then + unset 'repl[all]' + echo "Option --replication_name incompatible with --replication_all, clearing replication_all" >&2 + fi + ;; + esac +} + +_test_exists() { + declare -F "$1" + return $? +} + +# Marks the end of mysql -> mariadb name changes in 10.6+ +#ENDOFSUBSTITUTIONS +while [ $# -gt 0 ]; do + case "$1" in + --su=*) + u="${1#*-}" + shift + exec gosu "${u}" "${BASH_SOURCE[0]}" "$@" + ;; + --su-mysql) + shift + exec gosu mysql "${BASH_SOURCE[0]}" "$@" + ;; + --replication_*=*) + # Change the n to what is between _ and = and make lower case + n=${1#*_} + n=${n%%=*} + n=${n,,*} + # v is after the = + v=${1#*=} + repl[$n]=$v + _repl_param_check "$n" + ;; + --replication_*) + # Without =, look for a non --option next as the value, + # otherwise treat it as an "enable", just equate to 1. + # Clearing option is possible with "--replication_X=" + n=${1#*_} + n=${n,,*} + if [ "${2:0:2}" == '--' ]; then + repl[$n]=1 + else + repl[$n]=$2 + shift + fi + _repl_param_check "$n" + ;; + --datadir=*) + datadir=${1#*=} + ;; + --no-defaults) + unset def + nodefaults=1 + ;; + --defaults-file=*|--defaults-extra-file=*|--defaults-group-suffix=*) + n=${1:11} # length --defaults- + n=${n%%=*} + n=${n//-/_} + # v is after the = + v=${1#*=} + def[$n]=$v + nodefaults= + ;; + --defaults-file|--defaults-extra-file|--defaults-group-suffix) + n=${1:11} # length --defaults- + n=${n//-/_} + if [ "${2:0:2}" == '--' ]; then + def[$n]="" + else + def[$n]=$2 + shift + fi + nodefaults= + ;; + --*) + test=${1#--} + ;; + *) + echo "Unknown healthcheck option $1" >&2 + exit 1 + esac + if [ -n "$test" ]; then + if ! _test_exists "$test" ; then + echo "healthcheck unknown test '$test'" >&2 + exit 1 + elif ! "$test"; then + echo "healthcheck $test failed" >&2 + exit 1 + fi + fi + shift +done diff --git a/update.sh b/update.sh index 184fd14..a77a241 100755 --- a/update.sh +++ b/update.sh @@ -103,7 +103,7 @@ for version in "${versions[@]}"; do backup="$backup-$version" fi - cp docker-entrypoint.sh "$version/" + cp docker-entrypoint.sh healthcheck.sh "$version/" sed -i \ -e 's!%%MARIADB_VERSION%%!'"$fullVersion"'!g' \ -e 's!%%MARIADB_VERSION_BASIC%%!'"$mariaVersion"'!g' \ @@ -134,6 +134,7 @@ for version in "${versions[@]}"; do -e "0,/#ENDOFSUBSTITIONS/s/mysqld/mariadbd/" \ -e 's/mysql_tzinfo_to_sql/mariadb-tzinfo-to-sql/' \ "$version/docker-entrypoint.sh" + sed -i -e '0,/#ENDOFSUBSTITIONS/s/\bmysql\b/mariadb/' "$version/healthcheck.sh" esac done