#!/bin/bash
#
# Test the MySQL image.
#
# IMAGE_NAME specifies the name of the candidate image used for testing.
# The image has to be available before this script is executed.
#
set -o errexit
set -o nounset
shopt -s nullglob
IMAGE_NAME=${IMAGE_NAME-centos/mariadb-101-centos7-candidate}
CIDFILE_DIR=$(mktemp --suffix=mysql_test_cidfiles -d)
function cleanup() {
local cidfile
for cidfile in $CIDFILE_DIR/* ; do
local CONTAINER
CONTAINER=$(cat $cidfile)
echo "Stopping and removing container $CONTAINER..."
docker stop $CONTAINER >/dev/null
local exit_status
exit_status=$(docker inspect -f '{{.State.ExitCode}}' $CONTAINER)
if [ "$exit_status" != "0" ]; then
echo "Inspecting container $CONTAINER"
docker inspect $CONTAINER
echo "Dumping logs for $CONTAINER"
docker logs $CONTAINER
fi
docker rm -v $CONTAINER >/dev/null
rm $cidfile
echo "Done."
done
rmdir $CIDFILE_DIR
}
trap cleanup EXIT SIGINT
function get_cid() {
local id="$1" ; shift || return 1
echo $(cat "$CIDFILE_DIR/$id")
}
function get_container_ip() {
local id="$1" ; shift
docker inspect --format='{{.NetworkSettings.IPAddress}}' $(get_cid "$id")
}
function mysql_cmd() {
local container_ip="$1"; shift
local login="$1"; shift
local password="$1"; shift
docker run --rm "$IMAGE_NAME" mysql --host "$container_ip" -u"$login" -p"$password" "$@" db
}
function test_connection() {
local name=$1 ; shift
local login=$1 ; shift
local password=$1 ; shift
local ip
ip=$(get_container_ip $name)
echo " Testing MySQL connection to $ip..."
local max_attempts=20
local sleep_time=2
local i
for i in $(seq $max_attempts); do
echo " Trying to connect..."
if mysql_cmd "$ip" "$login" "$password" <<< 'SELECT 1;'; then
echo " Success!"
return 0
fi
sleep $sleep_time
done
echo " Giving up: Failed to connect. Logs:"
docker logs $(get_cid $name)
return 1
}
function test_mysql() {
local container_ip="$1"
local login="$2"
local password="$3"
echo " Testing MySQL"
mysql_cmd "$container_ip" "$login" "$password" <<< 'CREATE TABLE tbl (col1 VARCHAR(20), col2 VARCHAR(20));'
mysql_cmd "$container_ip" "$login" "$password" <<< 'INSERT INTO tbl VALUES ("foo1", "bar1");'
mysql_cmd "$container_ip" "$login" "$password" <<< 'INSERT INTO tbl VALUES ("foo2", "bar2");'
mysql_cmd "$container_ip" "$login" "$password" <<< 'INSERT INTO tbl VALUES ("foo3", "bar3");'
mysql_cmd "$container_ip" "$login" "$password" <<< 'SELECT * FROM tbl;'
mysql_cmd "$container_ip" "$login" "$password" <<< 'DROP TABLE tbl;'
echo " Success!"
}
function create_container() {
local name=$1 ; shift
cidfile="$CIDFILE_DIR/$name"
# create container with a cidfile in a directory for cleanup
local container_id
container_id="$(docker run ${DOCKER_ARGS:-} --cidfile $cidfile -d "$@" $IMAGE_NAME ${CONTAINER_ARGS:-})"
echo "Created container $container_id"
}
function run_change_password_test() {
local tmpdir=$(mktemp -d)
mkdir "${tmpdir}/data" && chmod -R a+rwx "${tmpdir}"
# Create MySQL container with persistent volume and set the initial password
create_container "testpass1" -e MYSQL_USER=user -e MYSQL_PASSWORD=foo \
-e MYSQL_DATABASE=db -v ${tmpdir}:/var/lib/mysql/data:Z
test_connection testpass1 user foo
docker stop $(get_cid testpass1) >/dev/null
# Create second container with changed password
create_container "testpass2" -e MYSQL_USER=user -e MYSQL_PASSWORD=bar \
-e MYSQL_DATABASE=db -v ${tmpdir}:/var/lib/mysql/data:Z
test_connection testpass2 user bar
# The old password should not work anymore
if mysql_cmd "$(get_container_ip testpass2)" user foo -e 'SELECT 1;'; then
return 1
fi
}
function run_replication_test() {
local cluster_args="-e MYSQL_MASTER_USER=master -e MYSQL_MASTER_PASSWORD=master -e MYSQL_DATABASE=db"
local max_attempts=30
# Run the MySQL master
docker run $cluster_args -e MYSQL_USER=user -e MYSQL_PASSWORD=foo \
-e MYSQL_ROOT_PASSWORD=root \
-e MYSQL_INNODB_BUFFER_POOL_SIZE=5M \
-d --cidfile ${CIDFILE_DIR}/master.cid $IMAGE_NAME mysqld-master >/dev/null
local master_ip
master_ip=$(get_container_ip master.cid)
# Run the MySQL slave
docker run $cluster_args -e MYSQL_MASTER_SERVICE_NAME=${master_ip} \
-e MYSQL_INNODB_BUFFER_POOL_SIZE=5M \
-d --cidfile ${CIDFILE_DIR}/slave.cid $IMAGE_NAME mysqld-slave >/dev/null
local slave_ip
slave_ip=$(get_container_ip slave.cid)
# Now wait till the MASTER will see the SLAVE
local i
for i in $(seq $max_attempts); do
result="$(mysql_cmd "$master_ip" root root -e 'SHOW SLAVE HOSTS;' | grep "$slave_ip" || true)"
if [[ -n "${result}" ]]; then
echo "${slave_ip} successfully registered as SLAVE for ${master_ip}"
break
fi
if [[ "${i}" == "${max_attempts}" ]]; then
echo "The ${slave_ip} failed to register in MASTER"
echo "Dumping logs for $(get_cid slave.cid)"
docker logs $(get_cid slave.cid)
return 1
fi
sleep 1
done
# do some real work to test replication in practice
mysql_cmd "$master_ip" root root -e "CREATE TABLE t1 (a INT); INSERT INTO t1 VALUES (24);"
# read value from slave and check whether it is expectd
for i in $(seq $max_attempts); do
set +e
result="$(mysql_cmd "${slave_ip}" root root -e "select * from t1 \G" | grep -e ^a | grep 24)"
set -e
if [[ ! -z "${result}" ]]; then
echo "${slave_ip} successfully got value from MASTER ${master_ip}"
break
fi
if [[ "${i}" == "${max_attempts}" ]]; then
echo "The ${slave_ip} failed to see value added on MASTER"
echo "Dumping logs for $(get_cid slave.cid)"
docker logs $(get_cid slave.cid)
return 1
fi
sleep 1
done
}
function assert_login_access() {
local container_ip=$1; shift
local USER=$1 ; shift
local PASS=$1 ; shift
local success=$1 ; shift
if mysql_cmd "$container_ip" "$USER" "$PASS" <<< 'SELECT 1;' ; then
if $success ; then
echo " $USER($PASS) access granted as expected"
return
fi
else
if ! $success ; then
echo " $USER($PASS) access denied as expected"
return
fi
fi
echo " $USER($PASS) login assertion failed"
exit 1
}
function assert_local_access() {
local id="$1" ; shift
docker exec $(get_cid "$id") bash -c 'mysql <<< "SELECT 1;"'
}
# Make sure the invocation of docker run fails.
function assert_container_creation_fails() {
# Time the docker run command. It should fail. If it doesn't fail,
# mysqld will keep running so we kill it with SIGKILL to make sure
# timeout returns a non-zero value.
local ret=0
timeout -s 9 --preserve-status 60s docker run --rm "$@" $IMAGE_NAME >/dev/null || ret=$?
# Timeout will exit with a high number.
if [ $ret -gt 30 ]; then
return 1
fi
}
function try_image_invalid_combinations() {
assert_container_creation_fails -e MYSQL_USER=user -e MYSQL_DATABASE=db "$@"
assert_container_creation_fails -e MYSQL_PASSWORD=pass -e MYSQL_DATABASE=db "$@"
}
function run_container_creation_tests() {
echo " Testing image entrypoint usage"
assert_container_creation_fails
try_image_invalid_combinations
try_image_invalid_combinations -e MYSQL_ROOT_PASSWORD=root_pass
local VERY_LONG_DB_NAME="very_long_database_name_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
assert_container_creation_fails -e MYSQL_USER=user -e MYSQL_PASSWORD=pass
assert_container_creation_fails -e MYSQL_USER=\$invalid -e MYSQL_PASSWORD=pass -e MYSQL_DATABASE=db -e MYSQL_ROOT_PASSWORD=root_pass
assert_container_creation_fails -e MYSQL_USER=very_long_username -e MYSQL_PASSWORD=pass -e MYSQL_DATABASE=db -e MYSQL_ROOT_PASSWORD=root_pass
assert_container_creation_fails -e MYSQL_USER=user -e MYSQL_PASSWORD="\"" -e MYSQL_DATABASE=db -e MYSQL_ROOT_PASSWORD=root_pass
assert_container_creation_fails -e MYSQL_USER=user -e MYSQL_PASSWORD=pass -e MYSQL_DATABASE=\$invalid -e MYSQL_ROOT_PASSWORD=root_pass
assert_container_creation_fails -e MYSQL_USER=user -e MYSQL_PASSWORD=pass -e MYSQL_DATABASE=$VERY_LONG_DB_NAME -e MYSQL_ROOT_PASSWORD=root_pass
assert_container_creation_fails -e MYSQL_USER=user -e MYSQL_PASSWORD=pass -e MYSQL_DATABASE=db -e MYSQL_ROOT_PASSWORD="\""
assert_container_creation_fails -e MYSQL_USER=root -e MYSQL_PASSWORD=pass -e MYSQL_DATABASE=db -e MYSQL_ROOT_PASSWORD=pass
echo " Success!"
}
function test_config_option() {
local container_name="$1"
local configuration="$2"
local option_name="$3"
local option_value="$4"
if ! echo "$configuration" | grep -qx "$option_name[[:space:]]*=[[:space:]]*$option_value"; then
local configs="$(docker exec -t "$(get_cid $container_name)" bash -c 'set +f; shopt -s nullglob; echo /etc/my.cnf /etc/my.cnf.d/* /opt/rh/mysql*/root/etc/my.cnf /opt/rh/mysql*/root/etc/my.cnf.d/* | paste -s')"
echo >&2 "FAIL: option '$option_name' should have value '$option_value', but it wasn't found in any of the configuration files ($configs):"
echo >&2
echo >&2 "$configuration"
echo >&2
return 1
fi
return 0
}
function run_configuration_tests() {
echo " Testing image configuration settings"
local container_name=config_test
create_container \
"$container_name" \
--env MYSQL_USER=config_test_user \
--env MYSQL_PASSWORD=config_test \
--env MYSQL_DATABASE=db \
--env MYSQL_LOWER_CASE_TABLE_NAMES=1 \
--env MYSQL_MAX_CONNECTIONS=1337 \
--env MYSQL_FT_MIN_WORD_LEN=8 \
--env MYSQL_FT_MAX_WORD_LEN=15 \
--env MYSQL_MAX_ALLOWED_PACKET=10M \
--env MYSQL_TABLE_OPEN_CACHE=100 \
--env MYSQL_SORT_BUFFER_SIZE=256K \
--env MYSQL_KEY_BUFFER_SIZE=16M \
--env MYSQL_READ_BUFFER_SIZE=16M \
--env MYSQL_INNODB_BUFFER_POOL_SIZE=16M \
--env MYSQL_INNODB_LOG_FILE_SIZE=4M \
--env MYSQL_INNODB_LOG_BUFFER_SIZE=4M \
--env WORKAROUND_DOCKER_BUG_14203=
#
test_connection "$container_name" config_test_user config_test
# TODO: this check is far from perfect and could be improved:
# - we should look for an option in the desired config, not in all of them
# - we should respect section of the config (now we have duplicated options from a different sections)
local configuration
configuration="$(docker exec -t "$(get_cid $container_name)" bash -c 'set +f; shopt -s nullglob; egrep -hv "^(#|\!|\[|$)" /etc/my.cnf /etc/my.cnf.d/* /opt/rh/mysql*/root/etc/my.cnf /opt/rh/mysql*/root/etc/my.cnf.d/*' | sed 's,\(^[[:space:]]\+\|[[:space:]]\+$\),,' | sort -u)"
test_config_option "$container_name" "$configuration" lower_case_table_names 1
test_config_option "$container_name" "$configuration" max_connections 1337
test_config_option "$container_name" "$configuration" ft_min_word_len 8
test_config_option "$container_name" "$configuration" ft_max_word_len 15
test_config_option "$container_name" "$configuration" max_allowed_packet 10M
test_config_option "$container_name" "$configuration" table_open_cache 100
test_config_option "$container_name" "$configuration" sort_buffer_size 256K
test_config_option "$container_name" "$configuration" key_buffer_size 16M
test_config_option "$container_name" "$configuration" read_buffer_size 16M
test_config_option "$container_name" "$configuration" innodb_buffer_pool_size 16M
test_config_option "$container_name" "$configuration" innodb_log_file_size 4M
test_config_option "$container_name" "$configuration" innodb_log_buffer_size 4M
docker stop "$(get_cid $container_name)" >/dev/null
echo " Success!"
echo " Testing image auto-calculated configuration settings"
container_name=dynamic_config_test
DOCKER_ARGS='--memory=256m' create_container \
"$container_name" \
--env MYSQL_USER=config_test_user \
--env MYSQL_PASSWORD=config_test \
--env MYSQL_DATABASE=db
test_connection "$container_name" config_test_user config_test
configuration="$(docker exec -t "$(get_cid $container_name)" bash -c 'set +f; shopt -s nullglob; egrep -hv "^(#|\!|\[|$)" /etc/my.cnf /etc/my.cnf.d/* /opt/rh/mysql*/root/etc/my.cnf /opt/rh/mysql*/root/etc/my.cnf.d/*' | sed 's,\(^[[:space:]]\+\|[[:space:]]\+$\),,' | sort -u)"
test_config_option "$container_name" "$configuration" key_buffer_size 25M
test_config_option "$container_name" "$configuration" read_buffer_size 12M
test_config_option "$container_name" "$configuration" innodb_buffer_pool_size 128M
test_config_option "$container_name" "$configuration" innodb_log_file_size 38M
test_config_option "$container_name" "$configuration" innodb_log_buffer_size 38M
docker stop "$(get_cid $container_name)" >/dev/null
echo " Success!"
}
test_scl_usage() {
local name="$1"
local run_cmd="$2"
local expected="$3"
echo " Testing the image SCL enable"
local out
out=$(docker run --rm ${IMAGE_NAME} /bin/bash -c "${run_cmd}")
if ! echo "${out}" | grep -q "${expected}"; then
echo "ERROR[/bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'"
return 1
fi
out=$(docker exec $(get_cid $name) /bin/bash -c "${run_cmd}" 2>&1)
if ! echo "${out}" | grep -q "${expected}"; then
echo "ERROR[exec /bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'"
return 1
fi
out=$(docker exec $(get_cid $name) /bin/sh -ic "${run_cmd}" 2>&1)
if ! echo "${out}" | grep -q "${expected}"; then
echo "ERROR[exec /bin/sh -ic "${run_cmd}"] Expected '${expected}', got '${out}'"
return 1
fi
}
function run_tests() {
local name=$1 ; shift
envs="-e MYSQL_USER=$USER -e MYSQL_PASSWORD=$PASS -e MYSQL_DATABASE=db"
if [ -v ROOT_PASS ]; then
envs="$envs -e MYSQL_ROOT_PASSWORD=$ROOT_PASS"
fi
create_container $name $envs
test_connection "$name" "$USER" "$PASS"
echo " Testing scl usage"
test_scl_usage $name 'mysql --version' '10.1'
echo " Testing login accesses"
local container_ip
container_ip=$(get_container_ip $name)
assert_login_access "$container_ip" "$USER" "$PASS" true
assert_login_access "$container_ip" "$USER" "${PASS}_foo" false
if [ -v ROOT_PASS ]; then
assert_login_access "$container_ip" root "$ROOT_PASS" true
assert_login_access "$container_ip" root "${ROOT_PASS}_foo" false
else
assert_login_access "$container_ip" root 'foo' false
assert_login_access "$container_ip" root '' false
fi
assert_local_access "$name"
echo " Success!"
test_mysql "$container_ip" "$USER" "$PASS"
}
run_doc_test() {
local tmpdir=$(mktemp -d)
local f
echo " Testing documentation in the container image"
# Extract the help files from the container
for f in /usr/share/container-scripts/mysql/README.md help.1 ; do
docker run --rm ${IMAGE_NAME} /bin/bash -c "cat /${f}" >${tmpdir}/$(basename ${f})
# Check whether the files include some important information
for term in MYSQL_ROOT_PASSWORD volume 3306 ; do
if ! cat ${tmpdir}/$(basename ${f}) | grep -q -e "${term}" ; then
echo "ERROR: File /${f} does not include '${term}'."
return 1
fi
done
done
# Check whether the files use the correct format
if ! file ${tmpdir}/help.1 | grep -q roff ; then
echo "ERROR: /help.1 is not in troff or groff format"
return 1
fi
echo " Success!"
echo
}
# Tests.
run_container_creation_tests
run_configuration_tests
# Set lower buffer pool size to avoid running out of memory.
export CONTAINER_ARGS="run-mysqld --innodb_buffer_pool_size=5242880"
# Normal tests
USER=user PASS=pass run_tests no_root
USER=user1 PASS=pass1 ROOT_PASS=r00t run_tests root
# Test with arbitrary uid for the container
DOCKER_ARGS="-u 12345" USER=user PASS=pass run_tests no_root_altuid
DOCKER_ARGS="-u 12345" USER=user1 PASS=pass1 ROOT_PASS=r00t run_tests root_altuid
# Test the password change
run_change_password_test
# Replication tests
run_replication_test
run_doc_test