diff --git a/mysql/Dockerfile b/mysql/Dockerfile index 0aba820d..23c4c617 100644 --- a/mysql/Dockerfile +++ b/mysql/Dockerfile @@ -1,19 +1,47 @@ FROM fedora -MAINTAINER http://fedoraproject.org/wiki/Cloud -RUN dnf -y update && dnf clean all -RUN dnf -y install community-mysql-server community-mysql pwgen supervisor bash-completion psmisc net-tools && dnf clean all +# MySQL image for OpenShift. +# +# Volumes: +# * /var/lib/mysql/data - Datastore for MySQL +# Environment: +# * $MYSQL_USER - Database user name +# * $MYSQL_PASSWORD - User's password +# * $MYSQL_DATABASE - Name of the database to create +# * $MYSQL_ROOT_PASSWORD (Optional) - Password for the 'root' MySQL account -ADD ./start.sh /start.sh -ADD ./config_mysql.sh /config_mysql.sh -ADD ./supervisord.conf /etc/supervisord.conf +MAINTAINER http://fedoraproject.org/wiki/Cloud -# RUN echo %sudo ALL=NOPASSWD: ALL >> /etc/sudoers +ENV MYSQL_VERSION=10.0 \ + HOME=/var/lib/mysql -RUN chmod 755 /start.sh -RUN chmod 755 /config_mysql.sh -RUN /config_mysql.sh +LABEL io.k8s.description="MySQL is a multi-user, multi-threaded SQL database server" \ + io.k8s.display-name="MySQL 5.6" \ + io.openshift.expose-services="3306:mysql" \ + io.openshift.tags="database,mysql,mysql56" EXPOSE 3306 -CMD ["/bin/bash", "/start.sh"] +# This image must forever use UID 27 for mysql user so our volumes are +# safe in the future. This should *never* change, the last test is there +# to make sure of that. +RUN dnf -y --setopt=tsflags=nodocs install gettext hostname bind-utils community-mysql-server community-mysql && \ + dnf clean all && \ + mkdir -p /var/lib/mysql/data && chown -R mysql.0 /var/lib/mysql && \ + test "$(id mysql)" = "uid=27(mysql) gid=27(mysql) groups=27(mysql)" + +COPY run-*.sh /usr/local/bin/ +COPY contrib /var/lib/mysql/ + +# Loosen permission bits for group to avoid problems running container with +# arbitrary UID +# When only specifying user, group is 0, that's why /var/lib/mysql must have +# owner mysql.0; that allows to avoid a+rwx for this dir +RUN chmod -R g+rwx /var/lib/mysql + +VOLUME ["/var/lib/mysql/data"] + +USER 27 + +ENTRYPOINT ["run-mysqld.sh"] +CMD ["mysqld"] diff --git a/mysql/README.md b/mysql/README.md index 9c8dd025..d431905c 100644 --- a/mysql/README.md +++ b/mysql/README.md @@ -1,41 +1,88 @@ -dockerfiles-fedora-MySQL -======================== +MySQL for general usage and OpenShift - Docker image +====================================================== -This repo contains a recipe for making Docker container for SSH and MySQL on Fedora. +This repository contains Dockerfiles for MySQL images for general usage and for OpenShift. -Check your Docker version - # docker version +Environment variables and volumes +---------------------------------- -Perform the build +The image recognizes the following environment variables that you can set during +initialization by passing `-e VAR=VALUE` to the Docker run command. - # docker build --rm -t /mysql . +| Variable name | Description | +| :--------------------- | ----------------------------------------- | +| `MYSQL_USER` | User name for MySQL account to be created | +| `MYSQL_PASSWORD` | Password for the user account | +| `MYSQL_DATABASE` | Database name | +| `MYSQL_ROOT_PASSWORD` | Password for the root user (optional) | -Check the image out. +The following environment variables influence the MySQL configuration file. They are all optional. - # docker images +| Variable name | Description | Default +| :------------------------------ | ----------------------------------------------------------------- | ------------------------------- +| `MYSQL_LOWER_CASE_TABLE_NAMES` | Sets how the table names are stored and compared | 0 +| `MYSQL_MAX_CONNECTIONS` | The maximum permitted number of simultaneous client connections | 151 +| `MYSQL_FT_MIN_WORD_LEN` | The minimum length of the word to be included in a FULLTEXT index | 4 +| `MYSQL_FT_MAX_WORD_LEN` | The maximum length of the word to be included in a FULLTEXT index | 20 +| `MYSQL_AIO` | Controls the `innodb_use_native_aio` setting value in case the native AIO is broken. See http://help.directadmin.com/item.php?id=529 | 1 -Run it: +You can also set the following mount points by passing the `-v /host:/container` flag to Docker. - # docker run -d -p 3306:3306 /mysql +| Volume mount point | Description | +| :----------------------- | -------------------- | +| `/var/lib/mysql/data` | MySQL data directory | -Get container ID: +**Notice: When mouting a directory from the host into the container, ensure that the mounted +directory has the appropriate permissions and that the owner and group of the directory +matches the user UID or name which is running inside the container.** - # docker ps +Usage +--------------------------------- -Keep in mind the password set for MySQL is: mysqlPassword +For this, we will assume that you are using the `openshift/mysql-55-centos7` image. +If you want to set only the mandatory environment variables and not store +the database in a host directory, execute the following command: -Get the IP address for the container: +``` +$ docker run -d --name mysql_database -e MYSQL_USER=user -e MYSQL_PASSWORD=pass -e MYSQL_DATABASE=db -p 3306:3306 fedora/mysql +``` - # docker inspect | grep -i ipaddr +This will create a container named `mysql_database` running MySQL with database +`db` and user with credentials `user:pass`. Port 3306 will be exposed and mapped +to the host. If you want your database to be persistent across container executions, +also add a `-v /host/db/path:/var/lib/mysql/data` argument. This will be the MySQL +data directory. -For MySQL: - # mysql -h 172.17.0.x -utestdb -pmysqlPassword +If the database directory is not initialized, the entrypoint script will first +run [`mysql_install_db`](https://dev.mysql.com/doc/refman/5.6/en/mysql-install-db.html) +and setup necessary database users and passwords. After the database is initialized, +or if it was already present, `mysqld` is executed and will run as PID 1. You can + stop the detached container by running `docker stop mysql_database`. -Create a table: +MySQL root user +--------------------------------- +The root user has no password set by default, only allowing local connections. +You can set it by setting the `MYSQL_ROOT_PASSWORD` environment variable. This +will allow you to login to the root account remotely. Local connections will +still not require a password. + +To disable remote root access, simply unset `MYSQL_ROOT_PASSWORD` and restart +the container. + + +Changing passwords +------------------ + +Since passwords are part of the image configuration, the only supported method +to change passwords for the database user (`MYSQL_USER`) and root user is by +changing the environment variables `MYSQL_PASSWORD` and `MYSQL_ROOT_PASSWORD`, +respectively. + +Changing database passwords through SQL statements or any way other than through +the environment variables aforementioned will cause a mismatch between the +values stored in the variables and the actual passwords. Whenever a database +container starts it will reset the passwords to the values stored in the +environment variables. -``` -\> CREATE TABLE test (name VARCHAR(10), owner VARCHAR(10), - -> species VARCHAR(10), birth DATE, death DATE); -``` diff --git a/mysql/config_mysql.sh b/mysql/config_mysql.sh deleted file mode 100644 index 61573917..00000000 --- a/mysql/config_mysql.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -__mysql_config() { -# Hack to get MySQL up and running... I need to look into it more. -echo "Running the mysql_config function." -dnf -y erase community-mysql community-mysql-server -rm -rf /var/lib/mysql/ /etc/my.cnf -dnf -y install community-mysql community-mysql-server -mysql_install_db -chown -R mysql:mysql /var/lib/mysql -/usr/bin/mysqld_safe & -sleep 10 -} - -__start_mysql() { -echo "Running the start_mysql function." -mysqladmin -u root password mysqlPassword -mysql -uroot -pmysqlPassword -e "CREATE DATABASE testdb" -mysql -uroot -pmysqlPassword -e "GRANT ALL PRIVILEGES ON testdb.* TO 'testdb'@'localhost' IDENTIFIED BY 'mysqlPassword'; FLUSH PRIVILEGES;" -mysql -uroot -pmysqlPassword -e "GRANT ALL PRIVILEGES ON *.* TO 'testdb'@'%' IDENTIFIED BY 'mysqlPassword' WITH GRANT OPTION; FLUSH PRIVILEGES;" -mysql -uroot -pmysqlPassword -e "GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'mysqlPassword' WITH GRANT OPTION; FLUSH PRIVILEGES;" -mysql -uroot -pmysqlPassword -e "select user, host FROM mysql.user;" -killall mysqld -sleep 10 -} - -# Call all functions -__mysql_config -__start_mysql diff --git a/mysql/contrib/common.sh b/mysql/contrib/common.sh new file mode 100644 index 00000000..b59aa196 --- /dev/null +++ b/mysql/contrib/common.sh @@ -0,0 +1,165 @@ +#!/bin/bash + +# Data directory where MySQL database files live. The data subdirectory is here +# because .bashrc and my.cnf both live in /var/lib/mysql/ and we don't want a +# volume to override it. +export MYSQL_DATADIR=/var/lib/mysql/data + +# Configuration settings. +export MYSQL_DEFAULTS_FILE=$HOME/my.cnf +export MYSQL_LOWER_CASE_TABLE_NAMES=${MYSQL_LOWER_CASE_TABLE_NAMES:-0} +export MYSQL_MAX_CONNECTIONS=${MYSQL_MAX_CONNECTIONS:-151} +export MYSQL_FT_MIN_WORD_LEN=${MYSQL_FT_MIN_WORD_LEN:-4} +export MYSQL_FT_MAX_WORD_LEN=${MYSQL_FT_MAX_WORD_LEN:-20} +export MYSQL_AIO=${MYSQL_AIO:-1} + +# Be paranoid and stricter than we should be. +# https://dev.mysql.com/doc/refman/5.6/en/identifiers.html +mysql_identifier_regex='^[a-zA-Z0-9_]+$' +mysql_password_regex='^[a-zA-Z0-9_~!@#$%^&*()-=<>,.?;:|]+$' + +function usage() { + [ $# == 2 ] && echo "error: $1" + echo "You must either specify the following environment variables:" + echo " MYSQL_USER (regex: '$mysql_identifier_regex')" + echo " MYSQL_PASSWORD (regex: '$mysql_password_regex')" + echo " MYSQL_DATABASE (regex: '$mysql_identifier_regex')" + echo "Or the following environment variable:" + echo " MYSQL_ROOT_PASSWORD (regex: '$mysql_password_regex')" + echo "Or both." + echo "Optional Settings:" + echo " MYSQL_LOWER_CASE_TABLE_NAMES (default: 0)" + echo " MYSQL_MAX_CONNECTIONS (default: 151)" + echo " MYSQL_FT_MIN_WORD_LEN (default: 4)" + echo " MYSQL_FT_MAX_WORD_LEN (default: 20)" + echo " MYSQL_AIO (default: 1)" + exit 1 +} + +function validate_variables() { + # Check basic sanity of specified variables + if [[ -v MYSQL_USER && -v MYSQL_PASSWORD && -v MYSQL_DATABASE ]]; then + [[ "$MYSQL_USER" =~ $mysql_identifier_regex ]] || usage "Invalid MySQL username" + [ ${#MYSQL_USER} -le 16 ] || usage "MySQL username too long (maximum 16 characters)" + [[ "$MYSQL_PASSWORD" =~ $mysql_password_regex ]] || usage "Invalid password" + [[ "$MYSQL_DATABASE" =~ $mysql_identifier_regex ]] || usage "Invalid database name" + [ ${#MYSQL_DATABASE} -le 64 ] || usage "Database name too long (maximum 64 characters)" + user_specified=1 + fi + + if [ -v MYSQL_ROOT_PASSWORD ]; then + [[ "$MYSQL_ROOT_PASSWORD" =~ $mysql_password_regex ]] || usage "Invalid root password" + root_specified=1 + fi + + # Either combination of user/pass/db or root password is ok + if [[ "${user_specified:-0}" == "0" && "${root_specified:-0}" == "0" ]]; then + usage + fi + + # Specifically check of incomplete specification + if [[ -v MYSQL_USER || -v MYSQL_PASSWORD || -v MYSQL_DATABASE ]] && \ + [[ "${user_specified:-0}" == "0" ]]; then + usage + fi +} + +# Make sure env variables don't propagate to mysqld process. +function unset_env_vars() { + unset MYSQL_USER MYSQL_PASSWORD MYSQL_DATABASE MYSQL_ROOT_PASSWORD +} + +# Poll until MySQL responds to our ping. +function wait_for_mysql() { + pid=$1 ; shift + + while [ true ]; do + if [ -d "/proc/$pid" ]; then + mysqladmin --socket=/tmp/mysql.sock ping &>/dev/null && return 0 + else + return 1 + fi + echo "Waiting for MySQL to start ..." + sleep 1 + done +} + +function start_local_mysql() { + # Now start mysqld and add appropriate users. + echo 'Starting local mysqld server ...' + /usr/libexec/mysqld \ + --defaults-file=$MYSQL_DEFAULTS_FILE \ + --skip-networking --socket=/tmp/mysql.sock "$@" & + mysql_pid=$! + wait_for_mysql $mysql_pid +} + +# Initialize the MySQL database (create user accounts and the initial database) +function initialize_database() { + echo 'Running mysql_install_db ...' + # Using --rpm since we need mysql_install_db behaves as in RPM + mysql_install_db --rpm --datadir=$MYSQL_DATADIR + start_local_mysql "$@" + + [ -v MYSQL_RUNNING_AS_SLAVE ] && return + + # Do not care what option is compulsory here, just create what is specified + if [ -v MYSQL_USER ]; then +mysql $mysql_flags </dev/null && return 0 + sleep 1 + done +} + +function validate_replication_variables() { + if ! [[ -v MYSQL_DATABASE && -v MYSQL_MASTER_USER && -v MYSQL_MASTER_PASSWORD && \ + ( "${MYSQL_RUNNING_AS_SLAVE:-0}" != "1" || -v MYSQL_MASTER_SERVICE_NAME ) ]]; then + echo + echo "For master/slave replication, you have to specify following environment variables:" + echo " MYSQL_MASTER_SERVICE_NAME (slave only)" + echo " MYSQL_DATABASE" + echo " MYSQL_MASTER_USER" + echo " MYSQL_MASTER_PASSWORD" + echo + fi + [[ "$MYSQL_DATABASE" =~ $mysql_identifier_regex ]] || usage "Invalid database name" + [[ "$MYSQL_MASTER_USER" =~ $mysql_identifier_regex ]] || usage "Invalid MySQL master username" + [ ${#MYSQL_MASTER_USER} -le 16 ] || usage "MySQL master username too long (maximum 16 characters)" + [[ "$MYSQL_MASTER_PASSWORD" =~ $mysql_password_regex ]] || usage "Invalid MySQL master password" +} diff --git a/mysql/contrib/my-master.cnf.template b/mysql/contrib/my-master.cnf.template new file mode 100644 index 00000000..b6ab473b --- /dev/null +++ b/mysql/contrib/my-master.cnf.template @@ -0,0 +1,12 @@ +[mysqld] + +server-id = ${MYSQL_SERVER_ID} +log_bin = ${MYSQL_DATADIR}/mysql-bin.log +binlog_do_db = mysql +binlog_do_db = ${MYSQL_DATABASE} +gtid_mode = ON +log-slave-updates = ON +enforce-gtid-consistency = ON + +# Include the common MySQL settings +!include ${HOME}/my-common.cnf diff --git a/mysql/contrib/my-slave.cnf.template b/mysql/contrib/my-slave.cnf.template new file mode 100644 index 00000000..d0290a0f --- /dev/null +++ b/mysql/contrib/my-slave.cnf.template @@ -0,0 +1,13 @@ +[mysqld] + +server-id = ${MYSQL_SERVER_ID} +log_bin = ${MYSQL_DATADIR}/mysql-bin.log +relay-log = ${MYSQL_DATADIR}/mysql-relay-bin.log +binlog_do_db = mysql +binlog_do_db = ${MYSQL_DATABASE} +gtid_mode = ON +log-slave-updates = ON +enforce-gtid-consistency = ON + +# Include the common MySQL settings +!include ${HOME}/my-common.cnf diff --git a/mysql/contrib/my.cnf.template b/mysql/contrib/my.cnf.template new file mode 100644 index 00000000..0f609b3a --- /dev/null +++ b/mysql/contrib/my.cnf.template @@ -0,0 +1,32 @@ +[mysqld] +user = mysql + +datadir = ${MYSQL_DATADIR} +basedir = /usr +plugin-dir = /usr/lib64/mysql/plugin + +# Disabling symbolic-links is recommended to prevent assorted security risks +symbolic-links = 0 + +# http://www.percona.com/blog/2008/05/31/dns-achilles-heel-mysql-installation/ +skip_name_resolve + +# +# Settings configured by the user +# + +# Sets how the table names are stored and compared. Default: 0 +lower_case_table_names = ${MYSQL_LOWER_CASE_TABLE_NAMES} + +# The maximum permitted number of simultaneous client connections. Default: 151 +max_connections = ${MYSQL_MAX_CONNECTIONS} + +# The minimum length of the word to be included in a FULLTEXT index. Default: 4 +ft_min_word_len = ${MYSQL_FT_MIN_WORD_LEN} + +# The maximum length of the word to be included in a FULLTEXT index. Default: 20 +ft_max_word_len = ${MYSQL_FT_MAX_WORD_LEN} + +# In case the native AIO is broken. Default: 1 +# See http://help.directadmin.com/item.php?id=529 +innodb_use_native_aio = ${MYSQL_AIO} diff --git a/mysql/mysql/Dockerfile b/mysql/mysql/Dockerfile new file mode 100644 index 00000000..cfd09f8f --- /dev/null +++ b/mysql/mysql/Dockerfile @@ -0,0 +1,48 @@ +FROM fedora:22 + +# MySQL image for OpenShift. +# +# Volumes: +# * /var/lib/mysql/data - Datastore for MySQL +# Environment: +# * $MYSQL_USER - Database user name +# * $MYSQL_PASSWORD - User's password +# * $MYSQL_DATABASE - Name of the database to create +# * $MYSQL_ROOT_PASSWORD (Optional) - Password for the 'root' MySQL account + +MAINTAINER http://fedoraproject.org/wiki/Cloud + +ENV MYSQL_VERSION=10.0 \ + HOME=/var/lib/mysql + +LABEL io.k8s.description="MySQL is a multi-user, multi-threaded SQL database server" \ + io.k8s.display-name="MySQL 5.6" \ + io.openshift.expose-services="3306:mysql" \ + io.openshift.tags="database,mysql,mysql56" + +EXPOSE 3306 + +# This image must forever use UID 27 for mysql user so our volumes are +# safe in the future. This should *never* change, the last test is there +# to make sure of that. +RUN dnf -y --setopt=tsflags=nodocs install gettext hostname bind-utils community-mysql-server community-mysql && \ + dnf clean all && \ + mkdir -p /var/lib/mysql/data && chown -R mysql.0 /var/lib/mysql && \ + test "$(id mysql)" = "uid=27(mysql) gid=27(mysql) groups=27(mysql)" + +COPY run-*.sh /usr/local/bin/ +COPY contrib /var/lib/mysql/ + +# Loosen permission bits for group to avoid problems running container with +# arbitrary UID +# When only specifying user, group is 0, that's why /var/lib/mysql must have +# owner mysql.0; that allows to avoid a+rwx for this dir +RUN chmod -R g+rwx /var/lib/mysql && \ + chmod a+x /usr/local/bin/run-*.sh + +VOLUME ["/var/lib/mysql/data"] + +USER 27 + +ENTRYPOINT ["run-mysqld.sh"] +CMD ["mysqld"] diff --git a/mysql/mysql/LICENSE b/mysql/mysql/LICENSE new file mode 100644 index 00000000..d159169d --- /dev/null +++ b/mysql/mysql/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/mysql/mysql/README.md b/mysql/mysql/README.md new file mode 100644 index 00000000..cfacfa0e --- /dev/null +++ b/mysql/mysql/README.md @@ -0,0 +1,104 @@ +MySQL for general usage and OpenShift - Docker image +====================================================== + +This repository contains Dockerfiles for MySQL images for general usage and for OpenShift. + + +Environment variables and volumes +---------------------------------- + +The image recognizes the following environment variables that you can set during +initialization by passing `-e VAR=VALUE` to the Docker run command. + +| Variable name | Description | +| :--------------------- | ----------------------------------------- | +| `MYSQL_USER` | User name for MySQL account to be created | +| `MYSQL_PASSWORD` | Password for the user account | +| `MYSQL_DATABASE` | Database name | +| `MYSQL_ROOT_PASSWORD` | Password for the root user (optional) | + +The following environment variables influence the MySQL configuration file. They are all optional. + +| Variable name | Description | Default +| :------------------------------ | ----------------------------------------------------------------- | ------------------------------- +| `MYSQL_LOWER_CASE_TABLE_NAMES` | Sets how the table names are stored and compared | 0 +| `MYSQL_MAX_CONNECTIONS` | The maximum permitted number of simultaneous client connections | 151 +| `MYSQL_FT_MIN_WORD_LEN` | The minimum length of the word to be included in a FULLTEXT index | 4 +| `MYSQL_FT_MAX_WORD_LEN` | The maximum length of the word to be included in a FULLTEXT index | 20 +| `MYSQL_AIO` | Controls the `innodb_use_native_aio` setting value in case the native AIO is broken. See http://help.directadmin.com/item.php?id=529 | 1 + +You can also set the following mount points by passing the `-v /host:/container` flag to Docker. + +| Volume mount point | Description | +| :----------------------- | -------------------- | +| `/var/lib/mysql/data` | MySQL data directory | + +**Notice: When mouting a directory from the host into the container, ensure that the mounted +directory has the appropriate permissions and that the owner and group of the directory +matches the user UID or name which is running inside the container.** + +Usage +--------------------------------- + +For this, we will assume that you are using the `fedora/mysql` image. +If you want to set only the mandatory environment variables and not store +the database in a host directory, execute the following command: + +``` +$ docker run -d --name mysql_database -e MYSQL_USER=user -e MYSQL_PASSWORD=pass -e MYSQL_DATABASE=db -p 3306:3306 fedora/mysql +``` + +This will create a container named `mysql_database` running MySQL with database +`db` and user with credentials `user:pass`. Port 3306 will be exposed and mapped +to the host. If you want your database to be persistent across container executions, +also add a `-v /host/db/path:/var/lib/mysql/data` argument. This will be the MySQL +data directory. + +If the database directory is not initialized, the entrypoint script will first +run [`mysql_install_db`](https://dev.mysql.com/doc/refman/5.6/en/mysql-install-db.html) +and setup necessary database users and passwords. After the database is initialized, +or if it was already present, `mysqld` is executed and will run as PID 1. You can + stop the detached container by running `docker stop mysql_database`. + + +MySQL root user +--------------------------------- +The root user has no password set by default, only allowing local connections. +You can set it by setting the `MYSQL_ROOT_PASSWORD` environment variable. This +will allow you to login to the root account remotely. Local connections will +still not require a password. + +To disable remote root access, simply unset `MYSQL_ROOT_PASSWORD` and restart +the container. + + +Changing passwords +------------------ + +Since passwords are part of the image configuration, the only supported method +to change passwords for the database user (`MYSQL_USER`) and root user is by +changing the environment variables `MYSQL_PASSWORD` and `MYSQL_ROOT_PASSWORD`, +respectively. + +Changing database passwords through SQL statements or any way other than through +the environment variables aforementioned will cause a mismatch between the +values stored in the variables and the actual passwords. Whenever a database +container starts it will reset the passwords to the values stored in the +environment variables. + + +Test +--------------------------------- + +This repository also provides a test framework, which checks basic functionality +of the MySQL image. + +To test the MySQL image, you need to run the test like this: + +```bash +#> cd mysql +#> docker build -t mysql . +#> IMAGE_NAME=mysql ./test/run +``` + +Tested on Docker 1.8.2. diff --git a/mysql/mysql/contrib/common.sh b/mysql/mysql/contrib/common.sh new file mode 100644 index 00000000..b59aa196 --- /dev/null +++ b/mysql/mysql/contrib/common.sh @@ -0,0 +1,165 @@ +#!/bin/bash + +# Data directory where MySQL database files live. The data subdirectory is here +# because .bashrc and my.cnf both live in /var/lib/mysql/ and we don't want a +# volume to override it. +export MYSQL_DATADIR=/var/lib/mysql/data + +# Configuration settings. +export MYSQL_DEFAULTS_FILE=$HOME/my.cnf +export MYSQL_LOWER_CASE_TABLE_NAMES=${MYSQL_LOWER_CASE_TABLE_NAMES:-0} +export MYSQL_MAX_CONNECTIONS=${MYSQL_MAX_CONNECTIONS:-151} +export MYSQL_FT_MIN_WORD_LEN=${MYSQL_FT_MIN_WORD_LEN:-4} +export MYSQL_FT_MAX_WORD_LEN=${MYSQL_FT_MAX_WORD_LEN:-20} +export MYSQL_AIO=${MYSQL_AIO:-1} + +# Be paranoid and stricter than we should be. +# https://dev.mysql.com/doc/refman/5.6/en/identifiers.html +mysql_identifier_regex='^[a-zA-Z0-9_]+$' +mysql_password_regex='^[a-zA-Z0-9_~!@#$%^&*()-=<>,.?;:|]+$' + +function usage() { + [ $# == 2 ] && echo "error: $1" + echo "You must either specify the following environment variables:" + echo " MYSQL_USER (regex: '$mysql_identifier_regex')" + echo " MYSQL_PASSWORD (regex: '$mysql_password_regex')" + echo " MYSQL_DATABASE (regex: '$mysql_identifier_regex')" + echo "Or the following environment variable:" + echo " MYSQL_ROOT_PASSWORD (regex: '$mysql_password_regex')" + echo "Or both." + echo "Optional Settings:" + echo " MYSQL_LOWER_CASE_TABLE_NAMES (default: 0)" + echo " MYSQL_MAX_CONNECTIONS (default: 151)" + echo " MYSQL_FT_MIN_WORD_LEN (default: 4)" + echo " MYSQL_FT_MAX_WORD_LEN (default: 20)" + echo " MYSQL_AIO (default: 1)" + exit 1 +} + +function validate_variables() { + # Check basic sanity of specified variables + if [[ -v MYSQL_USER && -v MYSQL_PASSWORD && -v MYSQL_DATABASE ]]; then + [[ "$MYSQL_USER" =~ $mysql_identifier_regex ]] || usage "Invalid MySQL username" + [ ${#MYSQL_USER} -le 16 ] || usage "MySQL username too long (maximum 16 characters)" + [[ "$MYSQL_PASSWORD" =~ $mysql_password_regex ]] || usage "Invalid password" + [[ "$MYSQL_DATABASE" =~ $mysql_identifier_regex ]] || usage "Invalid database name" + [ ${#MYSQL_DATABASE} -le 64 ] || usage "Database name too long (maximum 64 characters)" + user_specified=1 + fi + + if [ -v MYSQL_ROOT_PASSWORD ]; then + [[ "$MYSQL_ROOT_PASSWORD" =~ $mysql_password_regex ]] || usage "Invalid root password" + root_specified=1 + fi + + # Either combination of user/pass/db or root password is ok + if [[ "${user_specified:-0}" == "0" && "${root_specified:-0}" == "0" ]]; then + usage + fi + + # Specifically check of incomplete specification + if [[ -v MYSQL_USER || -v MYSQL_PASSWORD || -v MYSQL_DATABASE ]] && \ + [[ "${user_specified:-0}" == "0" ]]; then + usage + fi +} + +# Make sure env variables don't propagate to mysqld process. +function unset_env_vars() { + unset MYSQL_USER MYSQL_PASSWORD MYSQL_DATABASE MYSQL_ROOT_PASSWORD +} + +# Poll until MySQL responds to our ping. +function wait_for_mysql() { + pid=$1 ; shift + + while [ true ]; do + if [ -d "/proc/$pid" ]; then + mysqladmin --socket=/tmp/mysql.sock ping &>/dev/null && return 0 + else + return 1 + fi + echo "Waiting for MySQL to start ..." + sleep 1 + done +} + +function start_local_mysql() { + # Now start mysqld and add appropriate users. + echo 'Starting local mysqld server ...' + /usr/libexec/mysqld \ + --defaults-file=$MYSQL_DEFAULTS_FILE \ + --skip-networking --socket=/tmp/mysql.sock "$@" & + mysql_pid=$! + wait_for_mysql $mysql_pid +} + +# Initialize the MySQL database (create user accounts and the initial database) +function initialize_database() { + echo 'Running mysql_install_db ...' + # Using --rpm since we need mysql_install_db behaves as in RPM + mysql_install_db --rpm --datadir=$MYSQL_DATADIR + start_local_mysql "$@" + + [ -v MYSQL_RUNNING_AS_SLAVE ] && return + + # Do not care what option is compulsory here, just create what is specified + if [ -v MYSQL_USER ]; then +mysql $mysql_flags </dev/null && return 0 + sleep 1 + done +} + +function validate_replication_variables() { + if ! [[ -v MYSQL_DATABASE && -v MYSQL_MASTER_USER && -v MYSQL_MASTER_PASSWORD && \ + ( "${MYSQL_RUNNING_AS_SLAVE:-0}" != "1" || -v MYSQL_MASTER_SERVICE_NAME ) ]]; then + echo + echo "For master/slave replication, you have to specify following environment variables:" + echo " MYSQL_MASTER_SERVICE_NAME (slave only)" + echo " MYSQL_DATABASE" + echo " MYSQL_MASTER_USER" + echo " MYSQL_MASTER_PASSWORD" + echo + fi + [[ "$MYSQL_DATABASE" =~ $mysql_identifier_regex ]] || usage "Invalid database name" + [[ "$MYSQL_MASTER_USER" =~ $mysql_identifier_regex ]] || usage "Invalid MySQL master username" + [ ${#MYSQL_MASTER_USER} -le 16 ] || usage "MySQL master username too long (maximum 16 characters)" + [[ "$MYSQL_MASTER_PASSWORD" =~ $mysql_password_regex ]] || usage "Invalid MySQL master password" +} diff --git a/mysql/mysql/contrib/my-master.cnf.template b/mysql/mysql/contrib/my-master.cnf.template new file mode 100644 index 00000000..b6ab473b --- /dev/null +++ b/mysql/mysql/contrib/my-master.cnf.template @@ -0,0 +1,12 @@ +[mysqld] + +server-id = ${MYSQL_SERVER_ID} +log_bin = ${MYSQL_DATADIR}/mysql-bin.log +binlog_do_db = mysql +binlog_do_db = ${MYSQL_DATABASE} +gtid_mode = ON +log-slave-updates = ON +enforce-gtid-consistency = ON + +# Include the common MySQL settings +!include ${HOME}/my-common.cnf diff --git a/mysql/mysql/contrib/my-slave.cnf.template b/mysql/mysql/contrib/my-slave.cnf.template new file mode 100644 index 00000000..d0290a0f --- /dev/null +++ b/mysql/mysql/contrib/my-slave.cnf.template @@ -0,0 +1,13 @@ +[mysqld] + +server-id = ${MYSQL_SERVER_ID} +log_bin = ${MYSQL_DATADIR}/mysql-bin.log +relay-log = ${MYSQL_DATADIR}/mysql-relay-bin.log +binlog_do_db = mysql +binlog_do_db = ${MYSQL_DATABASE} +gtid_mode = ON +log-slave-updates = ON +enforce-gtid-consistency = ON + +# Include the common MySQL settings +!include ${HOME}/my-common.cnf diff --git a/mysql/mysql/contrib/my.cnf.template b/mysql/mysql/contrib/my.cnf.template new file mode 100644 index 00000000..0f609b3a --- /dev/null +++ b/mysql/mysql/contrib/my.cnf.template @@ -0,0 +1,32 @@ +[mysqld] +user = mysql + +datadir = ${MYSQL_DATADIR} +basedir = /usr +plugin-dir = /usr/lib64/mysql/plugin + +# Disabling symbolic-links is recommended to prevent assorted security risks +symbolic-links = 0 + +# http://www.percona.com/blog/2008/05/31/dns-achilles-heel-mysql-installation/ +skip_name_resolve + +# +# Settings configured by the user +# + +# Sets how the table names are stored and compared. Default: 0 +lower_case_table_names = ${MYSQL_LOWER_CASE_TABLE_NAMES} + +# The maximum permitted number of simultaneous client connections. Default: 151 +max_connections = ${MYSQL_MAX_CONNECTIONS} + +# The minimum length of the word to be included in a FULLTEXT index. Default: 4 +ft_min_word_len = ${MYSQL_FT_MIN_WORD_LEN} + +# The maximum length of the word to be included in a FULLTEXT index. Default: 20 +ft_max_word_len = ${MYSQL_FT_MAX_WORD_LEN} + +# In case the native AIO is broken. Default: 1 +# See http://help.directadmin.com/item.php?id=529 +innodb_use_native_aio = ${MYSQL_AIO} diff --git a/mysql/mysql/run-mysqld-master.sh b/mysql/mysql/run-mysqld-master.sh new file mode 100644 index 00000000..88c63a07 --- /dev/null +++ b/mysql/mysql/run-mysqld-master.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# +# This is an entrypoint that runs the MySQL server in the 'master' mode. +# +source ${HOME}/common.sh +set -eu + +mysql_flags="-u root --socket=/tmp/mysql.sock" +admin_flags="--defaults-file=$MYSQL_DEFAULTS_FILE $mysql_flags" + +validate_replication_variables +validate_variables + +# Generate the unique 'server-id' for this master +export MYSQL_SERVER_ID=$(server_id) +echo "The 'master' server-id is ${MYSQL_SERVER_ID}" + +# Process the MySQL configuration files +envsubst < $HOME/my.cnf.template > $HOME/my-common.cnf +envsubst < $HOME/my-master.cnf.template > $MYSQL_DEFAULTS_FILE + +# Initialize MySQL database if this is the first time this containr runs and there +# are no existing data. In other case just start the local mysql to allow editing +# configuration +if [ ! -d "$MYSQL_DATADIR/mysql" ]; then + initialize_database "$@" +else + start_local_mysql "$@" +fi + +# Set the password for MySQL user and root everytime this container is started. +# This allows to change the password by editing the deployment configuration. +if [[ -v MYSQL_USER && -v MYSQL_PASSWORD ]]; then + mysql $mysql_flags <&1 diff --git a/mysql/mysql/run-mysqld-slave.sh b/mysql/mysql/run-mysqld-slave.sh new file mode 100644 index 00000000..e391666d --- /dev/null +++ b/mysql/mysql/run-mysqld-slave.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# +# This is an entrypoint that runs the MySQL server in the 'slave' mode. +# +source ${HOME}/common.sh +set -eu + +export MYSQL_RUNNING_AS_SLAVE=1 + +mysql_flags="-u root --socket=/tmp/mysql.sock" +admin_flags="--defaults-file=$MYSQL_DEFAULTS_FILE $mysql_flags" + +validate_replication_variables + +# Generate the unique 'server-id' for this master +export MYSQL_SERVER_ID=$(server_id) +echo "The 'slave' server-id is ${MYSQL_SERVER_ID}" + +# Process the MySQL configuration files +envsubst < $HOME/my.cnf.template > $HOME/my-common.cnf +envsubst < $HOME/my-slave.cnf.template > $MYSQL_DEFAULTS_FILE + +# Initialize MySQL database and wait for the MySQL master to accept +# connections. +initialize_database "$@" +wait_for_mysql_master + +mysql $mysql_flags <&1 diff --git a/mysql/mysql/run-mysqld.sh b/mysql/mysql/run-mysqld.sh new file mode 100644 index 00000000..14151afa --- /dev/null +++ b/mysql/mysql/run-mysqld.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +source $HOME/common.sh +set -eu + +mysql_flags="-u root --socket=/tmp/mysql.sock" +admin_flags="--defaults-file=$MYSQL_DEFAULTS_FILE $mysql_flags" + +cmd="$1"; shift + +if [ "${cmd}" == "mysqld-master" ] && [ ! -d "${MYSQL_DATADIR}/mysql" ]; then + exec /usr/local/bin/run-mysqld-master.sh "$@" +fi + +if [ "${cmd}" == "mysqld-slave" ] && [ ! -d "${MYSQL_DATADIR}/mysql" ]; then + exec /usr/local/bin/run-mysqld-slave.sh "$@" +fi + +if [ "${cmd}" == "mysqld" ]; then + validate_variables + envsubst < ${MYSQL_DEFAULTS_FILE}.template > $MYSQL_DEFAULTS_FILE + + if [ ! -d "$MYSQL_DATADIR/mysql" ]; then + initialize_database "$@" + else + start_local_mysql "$@" + fi + +# Set the password for MySQL user and root everytime this container is started. +# This allows to change the password by editing the deployment configuration. +if [[ -v MYSQL_USER && -v MYSQL_PASSWORD ]]; then + mysql $mysql_flags <&1 +fi + +unset_env_vars +exec $cmd "$@" diff --git a/mysql/mysql/test/run b/mysql/mysql/test/run new file mode 100755 index 00000000..4d2ce3b5 --- /dev/null +++ b/mysql/mysql/test/run @@ -0,0 +1,303 @@ +#!/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 -exo nounset +shopt -s nullglob + +IMAGE_NAME=${IMAGE_NAME-fedora/mysql} + +CIDFILE_DIR=$(mktemp --suffix=mysql_test_cidfiles -d) + +function cleanup() { + for cidfile in $CIDFILE_DIR/* ; do + CONTAINER=$(cat $cidfile) + + echo "Stopping and removing container $CONTAINER..." + docker stop $CONTAINER + exit_status=$(docker inspect -f '{{.State.ExitCode}}' $CONTAINER) + if [ "$exit_status" != "0" ]; then + echo "Dumping logs for $CONTAINER" + docker logs $CONTAINER + fi + docker rm $CONTAINER + 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() { + docker run --rm $IMAGE_NAME mysql --host $CONTAINER_IP -u$USER -p"$PASS" "$@" db +} + +function test_connection() { + local name=$1 ; shift + ip=$(get_container_ip $name) + echo " Testing MySQL connection to $ip..." + local max_attempts=20 + local sleep_time=2 + for i in $(seq $max_attempts); do + echo " Trying to connect..." + set +e + mysql_cmd <<< "SELECT 1;" + status=$? + set -e + if [ $status -eq 0 ]; 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() { + echo " Testing MySQL" + mysql_cmd <<< "CREATE TABLE tbl (col1 VARCHAR(20), col2 VARCHAR(20));" + mysql_cmd <<< "INSERT INTO tbl VALUES ('foo1', 'bar1');" + mysql_cmd <<< "INSERT INTO tbl VALUES ('foo2', 'bar2');" + mysql_cmd <<< "INSERT INTO tbl VALUES ('foo3', 'bar3');" + mysql_cmd <<< "SELECT * FROM tbl;" + mysql_cmd <<< "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 + docker run ${DOCKER_ARGS:-} --cidfile $cidfile -d "$@" $IMAGE_NAME ${CONTAINER_ARGS:-} + echo "Created container $(cat $cidfile)" +} + +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 + CONTAINER_IP=$(get_container_ip testpass1) USER=user PASS=foo test_connection "testpass1" + docker stop $(get_cid testpass1) + + # 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 + CONTAINER_IP=$(get_container_ip testpass2) USER=user PASS=bar test_connection "testpass2" + + # The old password should not work anymore + set +e + CONTAINER_IP=$(get_container_ip testpass2) USER=user PASS=foo \ + mysql_cmd -e "SELECT 1;" && return 1 + set -e +} + +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 \ + -d --cidfile ${CIDFILE_DIR}/master.cid $IMAGE_NAME mysqld-master \ + --innodb_buffer_pool_size=5242880 + master_ip=$(get_container_ip master.cid) + + # Run the MySQL slave + docker run $cluster_args -e MYSQL_MASTER_SERVICE_NAME=${master_ip} \ + -d --cidfile ${CIDFILE_DIR}/slave.cid $IMAGE_NAME mysqld-slave \ + --innodb_buffer_pool_size=5242880 + slave_ip=$(get_container_ip slave.cid) + + # Now wait till the MASTER will see the SLAVE + for i in $(seq $max_attempts); do + set +e + result=$(CONTAINER_IP=${master_ip} USER=root PASS=root \ + mysql_cmd -e "SHOW SLAVE HOSTS;" | grep ${slave_ip}) + set -e + if [[ ! -z "${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 +} + +function assert_login_access() { + local USER=$1 ; shift + local PASS=$1 ; shift + local success=$1 ; shift + + if mysql_cmd <<<'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. + set +e + timeout -s 9 --preserve-status 60s docker run --rm "$@" $IMAGE_NAME + ret=$? + set -e + + # 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_PASSWORD=pass "$@" + 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 + + VERY_LONG_DB_NAME="very_long_database_name_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + 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="\"" + echo " Success!" +} + +function test_config_option() { + local env_var=$1 ; shift + local setting=$1 ; shift + local value=$1 ; shift + + # If $value is a string, it needs to be in simple quotes ''. + # If nothing is found, grep returns 1 and test fails. + docker run --rm -e $env_var=${value//\'/} $IMAGE_NAME cat /var/lib/mysql/my.cnf | grep -q "$setting = $value" +} + +function run_configuration_tests() { + echo " Testing image configuration settings" + test_config_option MYSQL_LOWER_CASE_TABLE_NAMES lower_case_table_names 1 + test_config_option MYSQL_MAX_CONNECTIONS max_connections 1337 + test_config_option MYSQL_FT_MIN_WORD_LEN ft_min_word_len 8 + test_config_option MYSQL_FT_MAX_WORD_LEN ft_max_word_len 15 + echo " Success!" +} + +test_scl_usage() { + local name="$1" + local run_cmd="$2" + local expected="$3" + + echo " Testing the image SCL enable" + 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 + CONTAINER_IP=$(get_container_ip $name) + test_connection $name + echo " Testing login accesses" + assert_login_access $USER $PASS true + assert_login_access $USER "${PASS}_foo" false + if [ -v ROOT_PASS ]; then + assert_login_access root $ROOT_PASS true + assert_login_access root "${ROOT_PASS}_foo" false + else + assert_login_access root "foo" false + assert_login_access root "" false + fi + assert_local_access $name + echo " Success!" + test_mysql $name +} + +# Tests. + +run_container_creation_tests + +# FIXME: Disabled because the configuration files are not processed when running +# commands like 'cat'. To have the my.cnf processed you have to run one of the +# valid entrypoints. +# run_configuration_tests + +# Set lower buffer pool size to avoid running out of memory. +export CONTAINER_ARGS="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 diff --git a/mysql/run-mysqld-master.sh b/mysql/run-mysqld-master.sh new file mode 100755 index 00000000..88c63a07 --- /dev/null +++ b/mysql/run-mysqld-master.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# +# This is an entrypoint that runs the MySQL server in the 'master' mode. +# +source ${HOME}/common.sh +set -eu + +mysql_flags="-u root --socket=/tmp/mysql.sock" +admin_flags="--defaults-file=$MYSQL_DEFAULTS_FILE $mysql_flags" + +validate_replication_variables +validate_variables + +# Generate the unique 'server-id' for this master +export MYSQL_SERVER_ID=$(server_id) +echo "The 'master' server-id is ${MYSQL_SERVER_ID}" + +# Process the MySQL configuration files +envsubst < $HOME/my.cnf.template > $HOME/my-common.cnf +envsubst < $HOME/my-master.cnf.template > $MYSQL_DEFAULTS_FILE + +# Initialize MySQL database if this is the first time this containr runs and there +# are no existing data. In other case just start the local mysql to allow editing +# configuration +if [ ! -d "$MYSQL_DATADIR/mysql" ]; then + initialize_database "$@" +else + start_local_mysql "$@" +fi + +# Set the password for MySQL user and root everytime this container is started. +# This allows to change the password by editing the deployment configuration. +if [[ -v MYSQL_USER && -v MYSQL_PASSWORD ]]; then + mysql $mysql_flags <&1 diff --git a/mysql/run-mysqld-slave.sh b/mysql/run-mysqld-slave.sh new file mode 100755 index 00000000..e391666d --- /dev/null +++ b/mysql/run-mysqld-slave.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# +# This is an entrypoint that runs the MySQL server in the 'slave' mode. +# +source ${HOME}/common.sh +set -eu + +export MYSQL_RUNNING_AS_SLAVE=1 + +mysql_flags="-u root --socket=/tmp/mysql.sock" +admin_flags="--defaults-file=$MYSQL_DEFAULTS_FILE $mysql_flags" + +validate_replication_variables + +# Generate the unique 'server-id' for this master +export MYSQL_SERVER_ID=$(server_id) +echo "The 'slave' server-id is ${MYSQL_SERVER_ID}" + +# Process the MySQL configuration files +envsubst < $HOME/my.cnf.template > $HOME/my-common.cnf +envsubst < $HOME/my-slave.cnf.template > $MYSQL_DEFAULTS_FILE + +# Initialize MySQL database and wait for the MySQL master to accept +# connections. +initialize_database "$@" +wait_for_mysql_master + +mysql $mysql_flags <&1 diff --git a/mysql/run-mysqld.sh b/mysql/run-mysqld.sh new file mode 100755 index 00000000..14151afa --- /dev/null +++ b/mysql/run-mysqld.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +source $HOME/common.sh +set -eu + +mysql_flags="-u root --socket=/tmp/mysql.sock" +admin_flags="--defaults-file=$MYSQL_DEFAULTS_FILE $mysql_flags" + +cmd="$1"; shift + +if [ "${cmd}" == "mysqld-master" ] && [ ! -d "${MYSQL_DATADIR}/mysql" ]; then + exec /usr/local/bin/run-mysqld-master.sh "$@" +fi + +if [ "${cmd}" == "mysqld-slave" ] && [ ! -d "${MYSQL_DATADIR}/mysql" ]; then + exec /usr/local/bin/run-mysqld-slave.sh "$@" +fi + +if [ "${cmd}" == "mysqld" ]; then + validate_variables + envsubst < ${MYSQL_DEFAULTS_FILE}.template > $MYSQL_DEFAULTS_FILE + + if [ ! -d "$MYSQL_DATADIR/mysql" ]; then + initialize_database "$@" + else + start_local_mysql "$@" + fi + +# Set the password for MySQL user and root everytime this container is started. +# This allows to change the password by editing the deployment configuration. +if [[ -v MYSQL_USER && -v MYSQL_PASSWORD ]]; then + mysql $mysql_flags <&1 +fi + +unset_env_vars +exec $cmd "$@" diff --git a/mysql/start.sh b/mysql/start.sh deleted file mode 100644 index 204784bd..00000000 --- a/mysql/start.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -__run_supervisor() { -echo "Running the run_supervisor function." -supervisord -n -} - -# Call all functions -__run_supervisor diff --git a/mysql/supervisord.conf b/mysql/supervisord.conf deleted file mode 100644 index fa1b7f80..00000000 --- a/mysql/supervisord.conf +++ /dev/null @@ -1,146 +0,0 @@ -; Sample supervisor config file. -; -; For more information on the config file, please see: -; http://supervisord.org/configuration.html -; -; Note: shell expansion ("~" or "$HOME") is not supported. Environment -; variables can be expanded using this syntax: "%(ENV_HOME)s". - -[unix_http_server] -file=/tmp/supervisor.sock ; (the path to the socket file) -;chmod=0700 ; socket file mode (default 0700) -;chown=nobody:nogroup ; socket file uid:gid owner -;username=user ; (default is no username (open server)) -;password=123 ; (default is no password (open server)) - -;[inet_http_server] ; inet (TCP) server disabled by default -;port=127.0.0.1:9001 ; (ip_address:port specifier, *:port for all iface) -;username=user ; (default is no username (open server)) -;password=123 ; (default is no password (open server)) - -[supervisord] -logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log) -logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB) -logfile_backups=10 ; (num of main logfile rotation backups;default 10) -loglevel=info ; (log level;default info; others: debug,warn,trace) -pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid) -nodaemon=false ; (start in foreground if true;default false) -minfds=1024 ; (min. avail startup file descriptors;default 1024) -minprocs=200 ; (min. avail process descriptors;default 200) -;umask=022 ; (process file creation umask;default 022) -;user=chrism ; (default is current user, required if root) -;identifier=supervisor ; (supervisord identifier, default is 'supervisor') -;directory=/tmp ; (default is not to cd during start) -;nocleanup=true ; (don't clean up tempfiles at start;default false) -;childlogdir=/tmp ; ('AUTO' child log dir, default $TEMP) -;environment=KEY=value ; (key value pairs to add to environment) -;strip_ansi=false ; (strip ansi escape codes in logs; def. false) - -; the below section must remain in the config file for RPC -; (supervisorctl/web interface) to work, additional interfaces may be -; added by defining them in separate rpcinterface: sections -[rpcinterface:supervisor] -supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface - -[supervisorctl] -serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket -;serverurl=http://127.0.0.1:9001 ; use an http:// url to specify an inet socket -;username=chris ; should be same as http_username if set -;password=123 ; should be same as http_password if set -;prompt=mysupervisor ; cmd line prompt (default "supervisor") -;history_file=~/.sc_history ; use readline history if available - -; The below sample program section shows all possible program subsection values, -; create one or more 'real' program: sections to be able to control them under -; supervisor. - -;[program:theprogramname] -;command=/bin/cat ; the program (relative uses PATH, can take args) -;process_name=%(program_name)s ; process_name expr (default %(program_name)s) -;numprocs=1 ; number of processes copies to start (def 1) -;directory=/tmp ; directory to cwd to before exec (def no cwd) -;umask=022 ; umask for process (default None) -;priority=999 ; the relative start priority (default 999) -;autostart=true ; start at supervisord start (default: true) -;autorestart=unexpected ; whether/when to restart (default: unexpected) -;startsecs=1 ; number of secs prog must stay running (def. 1) -;startretries=3 ; max # of serial start failures (default 3) -;exitcodes=0,2 ; 'expected' exit codes for process (default 0,2) -;stopsignal=QUIT ; signal used to kill process (default TERM) -;stopwaitsecs=10 ; max num secs to wait b4 SIGKILL (default 10) -;stopasgroup=false ; send stop signal to the UNIX process group (default false) -;killasgroup=false ; SIGKILL the UNIX process group (def false) -;user=chrism ; setuid to this UNIX account to run the program -;redirect_stderr=true ; redirect proc stderr to stdout (default false) -;stdout_logfile=/a/path ; stdout log path, NONE for none; default AUTO -;stdout_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB) -;stdout_logfile_backups=10 ; # of stdout logfile backups (default 10) -;stdout_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0) -;stdout_events_enabled=false ; emit events on stdout writes (default false) -;stderr_logfile=/a/path ; stderr log path, NONE for none; default AUTO -;stderr_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB) -;stderr_logfile_backups=10 ; # of stderr logfile backups (default 10) -;stderr_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0) -;stderr_events_enabled=false ; emit events on stderr writes (default false) -;environment=A=1,B=2 ; process environment additions (def no adds) -;serverurl=AUTO ; override serverurl computation (childutils) - -; The below sample eventlistener section shows all possible -; eventlistener subsection values, create one or more 'real' -; eventlistener: sections to be able to handle event notifications -; sent by supervisor. - -;[eventlistener:theeventlistenername] -;command=/bin/eventlistener ; the program (relative uses PATH, can take args) -;process_name=%(program_name)s ; process_name expr (default %(program_name)s) -;numprocs=1 ; number of processes copies to start (def 1) -;events=EVENT ; event notif. types to subscribe to (req'd) -;buffer_size=10 ; event buffer queue size (default 10) -;directory=/tmp ; directory to cwd to before exec (def no cwd) -;umask=022 ; umask for process (default None) -;priority=-1 ; the relative start priority (default -1) -;autostart=true ; start at supervisord start (default: true) -;autorestart=unexpected ; whether/when to restart (default: unexpected) -;startsecs=1 ; number of secs prog must stay running (def. 1) -;startretries=3 ; max # of serial start failures (default 3) -;exitcodes=0,2 ; 'expected' exit codes for process (default 0,2) -;stopsignal=QUIT ; signal used to kill process (default TERM) -;stopwaitsecs=10 ; max num secs to wait b4 SIGKILL (default 10) -;stopasgroup=false ; send stop signal to the UNIX process group (default false) -;killasgroup=false ; SIGKILL the UNIX process group (def false) -;user=chrism ; setuid to this UNIX account to run the program -;redirect_stderr=true ; redirect proc stderr to stdout (default false) -;stdout_logfile=/a/path ; stdout log path, NONE for none; default AUTO -;stdout_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB) -;stdout_logfile_backups=10 ; # of stdout logfile backups (default 10) -;stdout_events_enabled=false ; emit events on stdout writes (default false) -;stderr_logfile=/a/path ; stderr log path, NONE for none; default AUTO -;stderr_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB) -;stderr_logfile_backups ; # of stderr logfile backups (default 10) -;stderr_events_enabled=false ; emit events on stderr writes (default false) -;environment=A=1,B=2 ; process environment additions -;serverurl=AUTO ; override serverurl computation (childutils) - -; The below sample group section shows all possible group values, -; create one or more 'real' group: sections to create "heterogeneous" -; process groups. - -;[group:thegroupname] -;programs=progname1,progname2 ; each refers to 'x' in [program:x] definitions -;priority=999 ; the relative start priority (default 999) - -; The [include] section can just contain the "files" setting. This -; setting can list multiple files (separated by whitespace or -; newlines). It can also contain wildcards. The filenames are -; interpreted as relative to this file. Included files *cannot* -; include files themselves. - -;[include] -;files = relative/directory/*.ini -;mysql and apache2 -[program:mysqld] -command=/usr/bin/mysqld_safe -stdout_logfile=/var/log/supervisor/%(program_name)s.log -stderr_logfile=/var/log/supervisor/%(program_name)s.log -autorestart=true - diff --git a/mysql/test/run b/mysql/test/run new file mode 100755 index 00000000..1e6de417 --- /dev/null +++ b/mysql/test/run @@ -0,0 +1,303 @@ +#!/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 -exo nounset +shopt -s nullglob + +IMAGE_NAME=${IMAGE_NAME-openshift/mysql-56-centos7-candidate} + +CIDFILE_DIR=$(mktemp --suffix=mysql_test_cidfiles -d) + +function cleanup() { + for cidfile in $CIDFILE_DIR/* ; do + CONTAINER=$(cat $cidfile) + + echo "Stopping and removing container $CONTAINER..." + docker stop $CONTAINER + exit_status=$(docker inspect -f '{{.State.ExitCode}}' $CONTAINER) + if [ "$exit_status" != "0" ]; then + echo "Dumping logs for $CONTAINER" + docker logs $CONTAINER + fi + docker rm $CONTAINER + 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() { + docker run --rm $IMAGE_NAME mysql --host $CONTAINER_IP -u$USER -p"$PASS" "$@" db +} + +function test_connection() { + local name=$1 ; shift + ip=$(get_container_ip $name) + echo " Testing MySQL connection to $ip..." + local max_attempts=20 + local sleep_time=2 + for i in $(seq $max_attempts); do + echo " Trying to connect..." + set +e + mysql_cmd <<< "SELECT 1;" + status=$? + set -e + if [ $status -eq 0 ]; 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() { + echo " Testing MySQL" + mysql_cmd <<< "CREATE TABLE tbl (col1 VARCHAR(20), col2 VARCHAR(20));" + mysql_cmd <<< "INSERT INTO tbl VALUES ('foo1', 'bar1');" + mysql_cmd <<< "INSERT INTO tbl VALUES ('foo2', 'bar2');" + mysql_cmd <<< "INSERT INTO tbl VALUES ('foo3', 'bar3');" + mysql_cmd <<< "SELECT * FROM tbl;" + mysql_cmd <<< "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 + docker run ${DOCKER_ARGS:-} --cidfile $cidfile -d "$@" $IMAGE_NAME ${CONTAINER_ARGS:-} + echo "Created container $(cat $cidfile)" +} + +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 + CONTAINER_IP=$(get_container_ip testpass1) USER=user PASS=foo test_connection "testpass1" + docker stop $(get_cid testpass1) + + # 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 + CONTAINER_IP=$(get_container_ip testpass2) USER=user PASS=bar test_connection "testpass2" + + # The old password should not work anymore + set +e + CONTAINER_IP=$(get_container_ip testpass2) USER=user PASS=foo \ + mysql_cmd -e "SELECT 1;" && return 1 + set -e +} + +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 \ + -d --cidfile ${CIDFILE_DIR}/master.cid $IMAGE_NAME mysqld-master \ + --innodb_buffer_pool_size=5242880 + master_ip=$(get_container_ip master.cid) + + # Run the MySQL slave + docker run $cluster_args -e MYSQL_MASTER_SERVICE_NAME=${master_ip} \ + -d --cidfile ${CIDFILE_DIR}/slave.cid $IMAGE_NAME mysqld-slave \ + --innodb_buffer_pool_size=5242880 + slave_ip=$(get_container_ip slave.cid) + + # Now wait till the MASTER will see the SLAVE + for i in $(seq $max_attempts); do + set +e + result=$(CONTAINER_IP=${master_ip} USER=root PASS=root \ + mysql_cmd -e "SHOW SLAVE HOSTS;" | grep ${slave_ip}) + set -e + if [[ ! -z "${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 +} + +function assert_login_access() { + local USER=$1 ; shift + local PASS=$1 ; shift + local success=$1 ; shift + + if mysql_cmd <<<'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. + set +e + timeout -s 9 --preserve-status 60s docker run --rm "$@" $IMAGE_NAME + ret=$? + set -e + + # 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_PASSWORD=pass "$@" + 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 + + VERY_LONG_DB_NAME="very_long_database_name_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + 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="\"" + echo " Success!" +} + +function test_config_option() { + local env_var=$1 ; shift + local setting=$1 ; shift + local value=$1 ; shift + + # If $value is a string, it needs to be in simple quotes ''. + # If nothing is found, grep returns 1 and test fails. + docker run --rm -e $env_var=${value//\'/} $IMAGE_NAME cat /var/lib/mysql/my.cnf | grep -q "$setting = $value" +} + +function run_configuration_tests() { + echo " Testing image configuration settings" + test_config_option MYSQL_LOWER_CASE_TABLE_NAMES lower_case_table_names 1 + test_config_option MYSQL_MAX_CONNECTIONS max_connections 1337 + test_config_option MYSQL_FT_MIN_WORD_LEN ft_min_word_len 8 + test_config_option MYSQL_FT_MAX_WORD_LEN ft_max_word_len 15 + echo " Success!" +} + +test_scl_usage() { + local name="$1" + local run_cmd="$2" + local expected="$3" + + echo " Testing the image SCL enable" + 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 + CONTAINER_IP=$(get_container_ip $name) + test_connection $name + echo " Testing login accesses" + assert_login_access $USER $PASS true + assert_login_access $USER "${PASS}_foo" false + if [ -v ROOT_PASS ]; then + assert_login_access root $ROOT_PASS true + assert_login_access root "${ROOT_PASS}_foo" false + else + assert_login_access root "foo" false + assert_login_access root "" false + fi + assert_local_access $name + echo " Success!" + test_mysql $name +} + +# Tests. + +run_container_creation_tests + +# FIXME: Disabled because the configuration files are not processed when running +# commands like 'cat'. To have the my.cnf processed you have to run one of the +# valid entrypoints. +# run_configuration_tests + +# Set lower buffer pool size to avoid running out of memory. +export CONTAINER_ARGS="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