|
| 1 | +#!/bin/bash |
| 2 | +set -eo pipefail |
| 3 | +shopt -s nullglob |
| 4 | + |
| 5 | +# logging functions |
| 6 | +mysql_log() { |
| 7 | + local type="$1"; shift |
| 8 | + printf '%s [%s] [Entrypoint]: %s\n' "$(date --rfc-3339=seconds)" "$type" "$*" |
| 9 | +} |
| 10 | +mysql_note() { |
| 11 | + mysql_log Note "$@" |
| 12 | +} |
| 13 | +mysql_warn() { |
| 14 | + mysql_log Warn "$@" >&2 |
| 15 | +} |
| 16 | +mysql_error() { |
| 17 | + mysql_log ERROR "$@" >&2 |
| 18 | + exit 1 |
| 19 | +} |
| 20 | + |
| 21 | +# usage: file_env VAR [DEFAULT] |
| 22 | +# ie: file_env 'XYZ_DB_PASSWORD' 'example' |
| 23 | +# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of |
| 24 | +# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature) |
| 25 | +file_env() { |
| 26 | + local var="$1" |
| 27 | + local fileVar="${var}_FILE" |
| 28 | + local def="${2:-}" |
| 29 | + if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then |
| 30 | + mysql_error "Both $var and $fileVar are set (but are exclusive)" |
| 31 | + fi |
| 32 | + local val="$def" |
| 33 | + if [ "${!var:-}" ]; then |
| 34 | + val="${!var}" |
| 35 | + elif [ "${!fileVar:-}" ]; then |
| 36 | + val="$(< "${!fileVar}")" |
| 37 | + fi |
| 38 | + export "$var"="$val" |
| 39 | + unset "$fileVar" |
| 40 | +} |
| 41 | + |
| 42 | +# check to see if this file is being run or sourced from another script |
| 43 | +_is_sourced() { |
| 44 | + # https://unix.stackexchange.com/a/215279 |
| 45 | + [ "${FUNCNAME[${#FUNCNAME[@]} - 1]}" == 'source' ] |
| 46 | +} |
| 47 | + |
| 48 | +# usage: docker_process_init_files [file [file [...]]] |
| 49 | +# ie: docker_process_init_files /always-initdb.d/* |
| 50 | +# process initializer files, based on file extensions |
| 51 | +docker_process_init_files() { |
| 52 | + # mysql here for backwards compatibility "${mysql[@]}" |
| 53 | + mysql=( docker_process_sql ) |
| 54 | + |
| 55 | + echo |
| 56 | + local f |
| 57 | + for f; do |
| 58 | + case "$f" in |
| 59 | + *.sh) mysql_note "$0: running $f"; . "$f" ;; |
| 60 | + *.sql) mysql_note "$0: running $f"; docker_process_sql < "$f"; echo ;; |
| 61 | + *.sql.gz) mysql_note "$0: running $f"; gunzip -c "$f" | docker_process_sql; echo ;; |
| 62 | + *) mysql_warn "$0: ignoring $f" ;; |
| 63 | + esac |
| 64 | + echo |
| 65 | + done |
| 66 | +} |
| 67 | + |
| 68 | +mysql_check_config() { |
| 69 | + local toRun=( "$@" --verbose --help ) errors |
| 70 | + if ! errors="$("${toRun[@]}" 2>&1 >/dev/null)"; then |
| 71 | + mysql_error $'mysqld failed while attempting to check config\n\tcommand was: '"${toRun[*]}"$'\n\t'"$errors" |
| 72 | + fi |
| 73 | +} |
| 74 | + |
| 75 | +# Fetch value from server config |
| 76 | +# We use mysqld --verbose --help instead of my_print_defaults because the |
| 77 | +# latter only show values present in config files, and not server defaults |
| 78 | +mysql_get_config() { |
| 79 | + local conf="$1"; shift |
| 80 | + "$@" --verbose --help --log-bin-index="$(mktemp -u)" 2>/dev/null \ |
| 81 | + | awk -v conf="$conf" '$1 == conf && /^[^ \t]/ { sub(/^[^ \t]+[ \t]+/, ""); print; exit }' |
| 82 | + # match "datadir /some/path with/spaces in/it here" but not "--xyz=abc\n datadir (xyz)" |
| 83 | +} |
| 84 | + |
| 85 | +# Do a temporary startup of the MySQL server, for init purposes |
| 86 | +docker_temp_server_start() { |
| 87 | + if [ "${MYSQL_MAJOR}" = '5.6' ]; then |
| 88 | + "$@" --skip-networking --socket="${SOCKET}" & |
| 89 | + mysql_note "Waiting for server startup" |
| 90 | + local i |
| 91 | + for i in {30..0}; do |
| 92 | + # only use the root password if the database has already been initializaed |
| 93 | + # so that it won't try to fill in a password file when it hasn't been set yet |
| 94 | + extraArgs=() |
| 95 | + if [ -z "$DATABASE_ALREADY_EXISTS" ]; then |
| 96 | + extraArgs+=( '--dont-use-mysql-root-password' ) |
| 97 | + fi |
| 98 | + if docker_process_sql "${extraArgs[@]}" --database=mysql <<<'SELECT 1' &> /dev/null; then |
| 99 | + break |
| 100 | + fi |
| 101 | + sleep 1 |
| 102 | + done |
| 103 | + if [ "$i" = 0 ]; then |
| 104 | + mysql_error "Unable to start server." |
| 105 | + fi |
| 106 | + else |
| 107 | + # For 5.7+ the server is ready for use as soon as startup command unblocks |
| 108 | + if ! "$@" --daemonize --skip-networking --socket="${SOCKET}"; then |
| 109 | + mysql_error "Unable to start server." |
| 110 | + fi |
| 111 | + fi |
| 112 | +} |
| 113 | + |
| 114 | +# Stop the server. When using a local socket file mysqladmin will block until |
| 115 | +# the shutdown is complete. |
| 116 | +docker_temp_server_stop() { |
| 117 | + if ! mysqladmin --defaults-extra-file=<( _mysql_passfile ) shutdown -uroot --socket="${SOCKET}"; then |
| 118 | + mysql_error "Unable to shut down server." |
| 119 | + fi |
| 120 | +} |
| 121 | + |
| 122 | +# Verify that the minimally required password settings are set for new databases. |
| 123 | +docker_verify_minimum_env() { |
| 124 | + if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" -a -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then |
| 125 | + mysql_error $'Database is uninitialized and password option is not specified\n\tYou need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD' |
| 126 | + fi |
| 127 | +} |
| 128 | + |
| 129 | +# creates folders for the database |
| 130 | +# also ensures permission for user mysql of run as root |
| 131 | +docker_create_db_directories() { |
| 132 | + local user; user="$(id -u)" |
| 133 | + |
| 134 | + # TODO other directories that are used by default? like /var/lib/mysql-files |
| 135 | + # see https://github.com/docker-library/mysql/issues/562 |
| 136 | + mkdir -p "$DATADIR" |
| 137 | + |
| 138 | + if [ "$user" = "0" ]; then |
| 139 | + # this will cause less disk access than `chown -R` |
| 140 | + find "$DATADIR" \! -user mysql -exec chown mysql '{}' + |
| 141 | + fi |
| 142 | +} |
| 143 | + |
| 144 | +# initializes the database directory |
| 145 | +docker_init_database_dir() { |
| 146 | + mysql_note "Initializing database files" |
| 147 | + if [ "$MYSQL_MAJOR" = '5.6' ]; then |
| 148 | + mysql_install_db --datadir="$DATADIR" --rpm --keep-my-cnf "${@:2}" |
| 149 | + else |
| 150 | + "$@" --initialize-insecure |
| 151 | + fi |
| 152 | + mysql_note "Database files initialized" |
| 153 | + |
| 154 | + if command -v mysql_ssl_rsa_setup > /dev/null && [ ! -e "$DATADIR/server-key.pem" ]; then |
| 155 | + # https://github.com/mysql/mysql-server/blob/23032807537d8dd8ee4ec1c4d40f0633cd4e12f9/packaging/deb-in/extra/mysql-systemd-start#L81-L84 |
| 156 | + mysql_note "Initializing certificates" |
| 157 | + mysql_ssl_rsa_setup --datadir="$DATADIR" |
| 158 | + mysql_note "Certificates initialized" |
| 159 | + fi |
| 160 | +} |
| 161 | + |
| 162 | +# Loads various settings that are used elsewhere in the script |
| 163 | +# This should be called after mysql_check_config, but before any other functions |
| 164 | +docker_setup_env() { |
| 165 | + # Get config |
| 166 | + declare -g DATADIR SOCKET |
| 167 | + DATADIR="$(mysql_get_config 'datadir' "$@")" |
| 168 | + SOCKET="$(mysql_get_config 'socket' "$@")" |
| 169 | + |
| 170 | + # Initialize values that might be stored in a file |
| 171 | + file_env 'MYSQL_ROOT_HOST' '%' |
| 172 | + file_env 'MYSQL_DATABASE' |
| 173 | + file_env 'MYSQL_USER' |
| 174 | + file_env 'MYSQL_PASSWORD' |
| 175 | + file_env 'MYSQL_ROOT_PASSWORD' |
| 176 | + |
| 177 | + declare -g DATABASE_ALREADY_EXISTS |
| 178 | + if [ -d "$DATADIR/mysql" ]; then |
| 179 | + DATABASE_ALREADY_EXISTS='true' |
| 180 | + fi |
| 181 | +} |
| 182 | + |
| 183 | +# Execute sql script, passed via stdin |
| 184 | +# usage: docker_process_sql [--dont-use-mysql-root-password] [mysql-cli-args] |
| 185 | +# ie: docker_process_sql --database=mydb <<<'INSERT ...' |
| 186 | +# ie: docker_process_sql --dont-use-mysql-root-password --database=mydb <my-file.sql |
| 187 | +docker_process_sql() { |
| 188 | + passfileArgs=() |
| 189 | + if [ '--dont-use-mysql-root-password' = "$1" ]; then |
| 190 | + passfileArgs+=( "$1" ) |
| 191 | + shift |
| 192 | + fi |
| 193 | + # args sent in can override this db, since they will be later in the command |
| 194 | + if [ -n "$MYSQL_DATABASE" ]; then |
| 195 | + set -- --database="$MYSQL_DATABASE" "$@" |
| 196 | + fi |
| 197 | + |
| 198 | + mysql --defaults-file=<( _mysql_passfile "${passfileArgs[@]}") --protocol=socket -uroot -hlocalhost --socket="${SOCKET}" "$@" |
| 199 | +} |
| 200 | + |
| 201 | +# Initializes database with timezone info and root password, plus optional extra db/user |
| 202 | +docker_setup_db() { |
| 203 | + # Load timezone info into database |
| 204 | + if [ -z "$MYSQL_INITDB_SKIP_TZINFO" ]; then |
| 205 | + # sed is for https://bugs.mysql.com/bug.php?id=20545 |
| 206 | + mysql_tzinfo_to_sql /usr/share/zoneinfo \ |
| 207 | + | sed 's/Local time zone must be set--see zic manual page/FCTY/' \ |
| 208 | + | docker_process_sql --dont-use-mysql-root-password --database=mysql |
| 209 | + # tell docker_process_sql to not use MYSQL_ROOT_PASSWORD since it is not set yet |
| 210 | + fi |
| 211 | + # Generate random root password |
| 212 | + if [ -n "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then |
| 213 | + export MYSQL_ROOT_PASSWORD="$(pwgen -1 32)" |
| 214 | + mysql_note "GENERATED ROOT PASSWORD: $MYSQL_ROOT_PASSWORD" |
| 215 | + fi |
| 216 | + # Sets root password and creates root users for non-localhost hosts |
| 217 | + local rootCreate= |
| 218 | + # default root to listen for connections from anywhere |
| 219 | + if [ -n "$MYSQL_ROOT_HOST" ] && [ "$MYSQL_ROOT_HOST" != 'localhost' ]; then |
| 220 | + # no, we don't care if read finds a terminating character in this heredoc |
| 221 | + # https://unix.stackexchange.com/questions/265149/why-is-set-o-errexit-breaking-this-read-heredoc-expression/265151#265151 |
| 222 | + read -r -d '' rootCreate <<-EOSQL || true |
| 223 | + CREATE USER 'root'@'${MYSQL_ROOT_HOST}' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ; |
| 224 | + GRANT ALL ON *.* TO 'root'@'${MYSQL_ROOT_HOST}' WITH GRANT OPTION ; |
| 225 | + EOSQL |
| 226 | + fi |
| 227 | + |
| 228 | + local passwordSet= |
| 229 | + if [ "$MYSQL_MAJOR" = '5.6' ]; then |
| 230 | + # no, we don't care if read finds a terminating character in this heredoc (see above) |
| 231 | + read -r -d '' passwordSet <<-EOSQL || true |
| 232 | + DELETE FROM mysql.user WHERE user NOT IN ('mysql.sys', 'mysqlxsys', 'root') OR host NOT IN ('localhost') ; |
| 233 | + SET PASSWORD FOR 'root'@'localhost'=PASSWORD('${MYSQL_ROOT_PASSWORD}') ; |
| 234 | + EOSQL |
| 235 | + else |
| 236 | + # no, we don't care if read finds a terminating character in this heredoc (see above) |
| 237 | + read -r -d '' passwordSet <<-EOSQL || true |
| 238 | + ALTER USER 'root'@'localhost' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ; |
| 239 | + EOSQL |
| 240 | + fi |
| 241 | + |
| 242 | + # tell docker_process_sql to not use MYSQL_ROOT_PASSWORD since it is just now being set |
| 243 | + docker_process_sql --dont-use-mysql-root-password --database=mysql <<-EOSQL |
| 244 | + -- What's done in this file shouldn't be replicated |
| 245 | + -- or products like mysql-fabric won't work |
| 246 | + SET @@SESSION.SQL_LOG_BIN=0; |
| 247 | +
|
| 248 | + ${passwordSet} |
| 249 | + GRANT ALL ON *.* TO 'root'@'localhost' WITH GRANT OPTION ; |
| 250 | + FLUSH PRIVILEGES ; |
| 251 | + ${rootCreate} |
| 252 | + DROP DATABASE IF EXISTS test ; |
| 253 | + EOSQL |
| 254 | + |
| 255 | + # Creates a custom database and user if specified |
| 256 | + if [ -n "$MYSQL_DATABASE" ]; then |
| 257 | + mysql_note "Creating database ${MYSQL_DATABASE}" |
| 258 | + docker_process_sql --database=mysql <<<"CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;" |
| 259 | + fi |
| 260 | + |
| 261 | + if [ -n "$MYSQL_USER" ] && [ -n "$MYSQL_PASSWORD" ]; then |
| 262 | + mysql_note "Creating user ${MYSQL_USER}" |
| 263 | + docker_process_sql --database=mysql <<<"CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD' ;" |
| 264 | + |
| 265 | + if [ -n "$MYSQL_DATABASE" ]; then |
| 266 | + mysql_note "Giving user ${MYSQL_USER} access to schema ${MYSQL_DATABASE}" |
| 267 | + docker_process_sql --database=mysql <<<"GRANT ALL ON \`$MYSQL_DATABASE\`.* TO '$MYSQL_USER'@'%' ;" |
| 268 | + fi |
| 269 | + |
| 270 | + docker_process_sql --database=mysql <<<"FLUSH PRIVILEGES ;" |
| 271 | + fi |
| 272 | +} |
| 273 | + |
| 274 | +_mysql_passfile() { |
| 275 | + # echo the password to the "file" the client uses |
| 276 | + # the client command will use process substitution to create a file on the fly |
| 277 | + # ie: --defaults-file=<( _mysql_passfile ) |
| 278 | + if [ '--dont-use-mysql-root-password' != "$1" ] && [ -n "$MYSQL_ROOT_PASSWORD" ]; then |
| 279 | + cat <<-EOF |
| 280 | + [client] |
| 281 | + password="${MYSQL_ROOT_PASSWORD}" |
| 282 | + EOF |
| 283 | + fi |
| 284 | +} |
| 285 | + |
| 286 | +# Mark root user as expired so the password must be changed before anything |
| 287 | +# else can be done (only supported for 5.6+) |
| 288 | +mysql_expire_root_user() { |
| 289 | + if [ -n "$MYSQL_ONETIME_PASSWORD" ]; then |
| 290 | + docker_process_sql --database=mysql <<-EOSQL |
| 291 | + ALTER USER 'root'@'%' PASSWORD EXPIRE; |
| 292 | + EOSQL |
| 293 | + fi |
| 294 | +} |
| 295 | + |
| 296 | +# check arguments for an option that would cause mysqld to stop |
| 297 | +# return true if there is one |
| 298 | +_mysql_want_help() { |
| 299 | + local arg |
| 300 | + for arg; do |
| 301 | + case "$arg" in |
| 302 | + -'?'|--help|--print-defaults|-V|--version) |
| 303 | + return 0 |
| 304 | + ;; |
| 305 | + esac |
| 306 | + done |
| 307 | + return 1 |
| 308 | +} |
| 309 | + |
| 310 | +_main() { |
| 311 | + # if command starts with an option, prepend mysqld |
| 312 | + if [ "${1:0:1}" = '-' ]; then |
| 313 | + set -- mysqld "$@" |
| 314 | + fi |
| 315 | + |
| 316 | + # skip setup if they aren't running mysqld or want an option that stops mysqld |
| 317 | + if [ "$1" = 'mysqld' ] && ! _mysql_want_help "$@"; then |
| 318 | + mysql_note "Entrypoint script for MySQL Server ${MYSQL_VERSION} started." |
| 319 | + |
| 320 | + mysql_check_config "$@" |
| 321 | + # Load various environment variables |
| 322 | + docker_setup_env "$@" |
| 323 | + docker_create_db_directories |
| 324 | + |
| 325 | + # If container is started as root user, restart as dedicated mysql user |
| 326 | + if [ "$(id -u)" = "0" ]; then |
| 327 | + mysql_note "Switching to dedicated user 'mysql'" |
| 328 | + exec gosu mysql "$BASH_SOURCE" "$@" |
| 329 | + fi |
| 330 | + |
| 331 | + # there's no database, so it needs to be initialized |
| 332 | + if [ -z "$DATABASE_ALREADY_EXISTS" ]; then |
| 333 | + docker_verify_minimum_env |
| 334 | + docker_init_database_dir "$@" |
| 335 | + |
| 336 | + mysql_note "Starting temporary server" |
| 337 | + docker_temp_server_start "$@" |
| 338 | + mysql_note "Temporary server started." |
| 339 | + |
| 340 | + docker_setup_db |
| 341 | + docker_process_init_files /docker-entrypoint-initdb.d/* |
| 342 | + |
| 343 | + mysql_expire_root_user |
| 344 | + |
| 345 | + mysql_note "Stopping temporary server" |
| 346 | + docker_temp_server_stop |
| 347 | + mysql_note "Temporary server stopped" |
| 348 | + |
| 349 | + echo |
| 350 | + mysql_note "MySQL init process done. Ready for start up." |
| 351 | + echo |
| 352 | + fi |
| 353 | + fi |
| 354 | + exec "$@" |
| 355 | +} |
| 356 | + |
| 357 | +# If we are sourced from elsewhere, don't perform any further actions |
| 358 | +if ! _is_sourced; then |
| 359 | + _main "$@" |
| 360 | +fi |
0 commit comments