diff --git a/db/couchdb.sh b/db/couchdb.sh new file mode 100644 index 0000000..a892d8c --- /dev/null +++ b/db/couchdb.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env sh +export PATH="/opt/couchdb/bin:$PATH" +RUN_AS="${SERVICE_USER:-couchdb}" +COUCHDB_USER="${DATABASE_USER_ROOT:-root}" +COUCHDB_PASSWORD=${DATABASE_PASS_ROOT:-couchdb_password} +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +__exec_command() { + exitCode=0 + cmd="${*:-bash -l}" + echo "${exec_message:-Executing command: $cmd}" + $cmd || exitCode=1 + [ "$exitCode" = 0 ] || exitCode=10 + return ${exitCode:-$?} +} +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +__curl() { curl -q -LSsf --user $COUCHDB_USER:$COUCHDB_PASSWORD "$@" || return 1; } +__curl_database() { curl -q -LSsf -X PUT "http://$COUCHDB_USER:$COUCHDB_PASSWORD@127.0.0.1:5984/$1" || return 1; } +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +__curl_users() { + __curl -X PUT "http://localhost:5984/_users/org.couchdb.user:$1" \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + -d '{"name": "'$1'", "password": "'$2'", "roles": ['$4'], "type": "'${3:-user}'"}' + return $? +} +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +[ -z "$DATA_DIR_INITIALIZED" ] && [ -f "/data/.docker_has_run" ] && DATA_DIR_INITIALIZED="true" || DATA_DIR_INITIALIZED="false" +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Create user if needed +if ! grep -q "$RUN_AS" /etc/passwd; then + groupadd -g 5984 -r $RUN_AS && useradd -u 5984 -d /opt/$RUN_AS -g $RUN_AS $RUN_AS +fi +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +[ -d "/data/couchdb" ] || mv -f "/opt/couchdb/data" "/data/couchdb" +[ -d "/opt/couchdb/data" ] && rm -Rf "/opt/couchdb/data" +ln -sf "/data/couchdb" "/opt/couchdb/data" 2>/dev/null +touch "/opt/couchdb/etc/local.d/docker.ini" 2>/dev/null +chown -Rf $RUN_AS:$RUN_AS "/data/couchdb" "/opt/couchdb" 2>/dev/null +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +case "$1" in +db) + shift 1 + case "$1" in + create) + shift 1 + __curl_database "$1" + exit $? + ;; + update) + shift 1 + __curl_database "$1" + exit $? + ;; + *) + echo "Usage: db [create,update] name" + exit 1 + ;; + esac + ;; + +user) + shift 1 + case "$1" in + create) + shift 1 + __curl_users "$1" "${2:-password}" + exit $? + ;; + update) + shift 1 + __curl_users "$1" "${2:-password}" + exit $? + ;; + *) + echo "Usage: user [create,update] username password type roles" + ;; + esac + ;; + +init) + shift 1 + if [ "$DATA_DIR_INITIALIZED" = "false" ]; then + { + sleep 60 + echo "Creating the default databases" + __curl -X PUT "http://127.0.0.1:5984/_users" 2>/dev/null >/dev/null && + echo "Created database _users" + __curl -X PUT "http://127.0.0.1:5984/_replicator" 2>/dev/null >/dev/null && + echo "Created database _replicator" + __curl -X PUT "http://127.0.0.1:5984/_global_changes" 2>/dev/null >/dev/null && + echo "Created database _global_changes" + echo "" + } >"/dev/stdout" & + fi + exit $? + ;; + +*) + if [ "$(id -u)" = '0' ]; then + find /opt/couchdb \! \( -user $RUN_AS -group $RUN_AS \) -exec chown -f $RUN_AS:$RUN_AS '{}' + + find /opt/couchdb/data -type d ! -perm 0755 -exec chmod -f 0755 '{}' + + find /opt/couchdb/data -type f ! -perm 0644 -exec chmod -f 0644 '{}' + + find /opt/couchdb/etc -type d ! -perm 0755 -exec chmod -f 0755 '{}' + + find /opt/couchdb/etc -type f ! -perm 0644 -exec chmod -f 0644 '{}' + + fi + + if [ -n "$NODENAME" ] && ! grep "couchdb@" /opt/couchdb/etc/vm.args; then + echo "-name couchdb@$NODENAME" >>/opt/couchdb/etc/vm.args + fi + + if [ -n "$RUN_AS" ]; then + if ! grep -sPzoqr "\[admins\]\n$RUN_AS =" /opt/couchdb/etc/local.d/*.ini /opt/couchdb/etc/local.ini; then + printf "\n[admins]\n%s = %s\n" "$RUN_AS" "$RUN_AS" >>/opt/couchdb/etc/local.d/docker.ini + fi + fi + + if [ -n "$COUCHDB_SECRET" ]; then + if ! grep -sPzoqr "\[chttpd_auth\]\nsecret =" /opt/couchdb/etc/local.d/*.ini /opt/couchdb/etc/local.ini; then + printf "\n[chttpd_auth]\nsecret = %s\n" "$COUCHDB_SECRET" >>/opt/couchdb/etc/local.d/docker.ini + fi + fi + + if [ -n "$COUCHDB_ERLANG_COOKIE" ]; then + cookieFile='/opt/couchdb/.erlang.cookie' + if [ -e "$cookieFile" ]; then + if [ "$(cat "$cookieFile" 2>/dev/null)" != "$COUCHDB_ERLANG_COOKIE" ]; then + echo >&2 + echo >&2 "warning: $cookieFile contents do not match COUCHDB_ERLANG_COOKIE" + echo >&2 + fi + else + echo "$COUCHDB_ERLANG_COOKIE" >"$cookieFile" + fi + chown $RUN_AS:$RUN_AS "$cookieFile" + chmod 600 "$cookieFile" + fi + + if [ "$(id -u)" = '0' ]; then + chown -f $RUN_AS:$RUN_AS /opt/couchdb/etc/local.d/docker.ini || true + fi + + if ! grep -Pzoqr '\[admins\]\n[^;]\w+' /opt/couchdb/etc/default.d/*.ini /opt/couchdb/etc/local.d/*.ini /opt/couchdb/etc/local.ini; then + cat >&2 <<-'EOWARN' +************************************************************* +ERROR: CouchDB 3.0+ will no longer run in "Admin Party" + mode. You *MUST* specify an admin user and + password, either via your own .ini file mapped + into the container at /opt/couchdb/etc/local.ini + or inside /opt/couchdb/etc/local.d, or with + "-e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password" + to set it via "docker run". +************************************************************* +EOWARN + exit 1 + fi + if [ "$(id -u)" = '0' ]; then + __exec_command gosu $RUN_AS /opt/couchdb/bin/couchdb + fi + + echo "This script should be called by root user" + ;; +esac diff --git a/db/mariadb.sh b/db/mariadb.sh new file mode 100644 index 0000000..37ce834 --- /dev/null +++ b/db/mariadb.sh @@ -0,0 +1,570 @@ +#!/usr/bin/env bash +set -eo pipefail +shopt -s nullglob +MARIADB_ROOT_HOST="%" +MARIADB_AUTO_UPGRADE="yes" +MARIADB_DATABASE="$DATABASE_CREATE" +MARIADB_USER="$DATABASE_USER_NORMAL" +MARIADB_PASSWORD="$DATABASE_PASS_NORMAL" +MARIADB_ROOT_PASSWORD="$DATABASE_PASS_ROOT" +MARIADB_ALLOW_EMPTY_ROOT_PASSWORD="" +MARIADB_INITDB_SKIP_TZINFO="" +MARIADB_RANDOM_ROOT_PASSWORD="" +# logging functions +mysql_log() { + local type="$1" + shift + printf '%s [%s] [Entrypoint]: %s\n' "$(date --rfc-3339=seconds)" "$type" "$*" +} +mysql_note() { + mysql_log Note "$@" +} +mysql_warn() { + mysql_log Warn "$@" >&2 +} +mysql_error() { + mysql_log ERROR "$@" >&2 + exit 1 +} + +# usage: file_env VAR [DEFAULT] +# ie: file_env 'XYZ_DB_PASSWORD' 'example' +# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of +# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature) +file_env() { + local var="$1" + local fileVar="${var}_FILE" + local def="${2:-}" + if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then + mysql_error "Both $var and $fileVar are set (but are exclusive)" + fi + local val="$def" + if [ "${!var:-}" ]; then + val="${!var}" + elif [ "${!fileVar:-}" ]; then + val="$(<"${!fileVar}")" + fi + export "$var"="$val" + unset "$fileVar" +} + +# set MARIADB_xyz from MYSQL_xyz when MARIADB_xyz is unset +# and make them the same value (so user scripts can use either) +_mariadb_file_env() { + local var="$1" + shift + local maria="MARIADB_${var#MYSQL_}" + file_env "$var" "$@" + file_env "$maria" "${!var}" + if [ "${!maria:-}" ]; then + export "$var"="${!maria}" + fi +} + +# check to see if this file is being run or sourced from another script +_is_sourced() { + # https://unix.stackexchange.com/a/215279 + [ "${#FUNCNAME[@]}" -ge 2 ] && + [ "${FUNCNAME[0]}" = '_is_sourced' ] && + [ "${FUNCNAME[1]}" = 'source' ] +} + +# usage: docker_process_init_files [file [file [...]]] +# ie: docker_process_init_files /always-initdb.d/* +# process initializer files, based on file extensions +docker_process_init_files() { + # mysql here for backwards compatibility "${mysql[@]}" + # ShellCheck: mysql appears unused. Verify use (or export if used externally) + # shellcheck disable=SC2034 + mysql=(docker_process_sql) + + echo + local f + for f; do + case "$f" in + *.sh) + # https://github.com/docker-library/postgres/issues/450#issuecomment-393167936 + # https://github.com/docker-library/postgres/pull/452 + if [ -x "$f" ]; then + mysql_note "$0: running $f" + "$f" + else + mysql_note "$0: sourcing $f" + # ShellCheck can't follow non-constant source. Use a directive to specify location. + # shellcheck disable=SC1090 + . "$f" + fi + ;; + *.sql) + mysql_note "$0: running $f" + docker_process_sql <"$f" + echo + ;; + *.sql.gz) + mysql_note "$0: running $f" + gunzip -c "$f" | docker_process_sql + echo + ;; + *.sql.xz) + mysql_note "$0: running $f" + xzcat "$f" | docker_process_sql + echo + ;; + *.sql.zst) + mysql_note "$0: running $f" + zstd -dc "$f" | docker_process_sql + echo + ;; + *) mysql_warn "$0: ignoring $f" ;; + esac + echo + done +} + +# arguments necessary to run "mariadbd --verbose --help" successfully (used for testing configuration validity and for extracting default/configured values) +_verboseHelpArgs=( + --verbose --help +) + +mysql_check_config() { + local toRun=("$@" "${_verboseHelpArgs[@]}") errors + if ! errors="$("${toRun[@]}" 2>&1 >/dev/null)"; then + mysql_error $'mariadbd failed while attempting to check config\n\tcommand was: '"${toRun[*]}"$'\n\t'"$errors" + fi +} + +# Fetch value from server config +# We use mariadbd --verbose --help instead of my_print_defaults because the +# latter only show values present in config files, and not server defaults +mysql_get_config() { + local conf="$1" + shift + "$@" "${_verboseHelpArgs[@]}" 2>/dev/null | + awk -v conf="$conf" '$1 == conf && /^[^ \t]/ { sub(/^[^ \t]+[ \t]+/, ""); print; exit }' + # match "datadir /some/path with/spaces in/it here" but not "--xyz=abc\n datadir (xyz)" +} + +# Do a temporary startup of the MariaDB server, for init purposes +docker_temp_server_start() { + "$@" --skip-networking --default-time-zone=SYSTEM --socket="${SOCKET}" --wsrep_on=OFF \ + --expire-logs-days=0 \ + --loose-innodb_buffer_pool_load_at_startup=0 & + declare -g MARIADB_PID + MARIADB_PID=$! + mysql_note "Waiting for server startup" + # only use the root password if the database has already been initialized + # so that it won't try to fill in a password file when it hasn't been set yet + extraArgs=() + if [ -z "$DATABASE_ALREADY_EXISTS" ]; then + extraArgs+=('--dont-use-mysql-root-password') + fi + local i + for i in {30..0}; do + if docker_process_sql "${extraArgs[@]}" --database=mysql <<<'SELECT 1' &>/dev/null; then + break + fi + sleep 1 + done + if [ "$i" = 0 ]; then + mysql_error "Unable to start server." + fi +} + +# Stop the server. When using a local socket file mariadb-admin will block until +# the shutdown is complete. +docker_temp_server_stop() { + kill "$MARIADB_PID" + wait "$MARIADB_PID" +} + +# Verify that the minimally required password settings are set for new databases. +docker_verify_minimum_env() { + if [ -z "$MARIADB_ROOT_PASSWORD" ] && [ -z "$MARIADB_ROOT_PASSWORD_HASH" ] && [ -z "$MARIADB_ALLOW_EMPTY_ROOT_PASSWORD" ] && [ -z "$MARIADB_RANDOM_ROOT_PASSWORD" ]; then + mysql_error $'Database is uninitialized and password option is not specified\n\tYou need to specify one of MARIADB_ROOT_PASSWORD, MARIADB_ROOT_PASSWORD_HASH, MARIADB_ALLOW_EMPTY_ROOT_PASSWORD and MARIADB_RANDOM_ROOT_PASSWORD' + fi + # More preemptive exclusions of combinations should have been made before *PASSWORD_HASH was added, but for now we don't enforce due to compatibility. + if [ -n "$MARIADB_ROOT_PASSWORD" ] || [ -n "$MARIADB_ALLOW_EMPTY_ROOT_PASSWORD" ] || [ -n "$MARIADB_RANDOM_ROOT_PASSWORD" ] && [ -n "$MARIADB_ROOT_PASSWORD_HASH" ]; then + mysql_error "Cannot specify MARIADB_ROOT_PASSWORD_HASH and another MARIADB_ROOT_PASSWORD* option." + fi + if [ -n "$MARIADB_PASSWORD" ] && [ -n "$MARIADB_PASSWORD_HASH" ]; then + mysql_error "Cannot specify MARIADB_PASSWORD_HASH and MARIADB_PASSWORD option." + fi +} + +# creates folders for the database +# also ensures permission for user mysql of run as root +docker_create_db_directories() { + local user + user="$(id -u)" + + # TODO other directories that are used by default? like /var/lib/mysql-files + # see https://github.com/docker-library/mysql/issues/562 + mkdir -p "$DATADIR" + + if [ "$user" = "0" ]; then + # this will cause less disk access than `chown -R` + find "$DATADIR" \! -user mysql -exec chown mysql: '{}' + + # See https://github.com/MariaDB/mariadb-docker/issues/363 + find "${SOCKET%/*}" -maxdepth 0 \! -user mysql -exec chown mysql: '{}' \; + fi +} + +_mariadb_version() { + local mariaVersion="${MARIADB_VERSION##*:}" + mariaVersion="${mariaVersion%%[-+~]*}" + echo -n "${mariaVersion}-MariaDB" +} + +# initializes the database directory +docker_init_database_dir() { + mysql_note "Initializing database files" + installArgs=(--datadir="$DATADIR" --rpm --auth-root-authentication-method=normal) + # "Other options are passed to mariadbd." (so we pass all "mysqld" arguments directly here) + mariadb-install-db "${installArgs[@]}" "${@:2}" \ + --skip-test-db \ + --old-mode='UTF8_IS_UTF8MB3' \ + --default-time-zone=SYSTEM --enforce-storage-engine= \ + --skip-log-bin \ + --expire-logs-days=0 \ + --loose-innodb_buffer_pool_load_at_startup=0 \ + --loose-innodb_buffer_pool_dump_at_shutdown=0 + mysql_note "Database files initialized" +} + +# Loads various settings that are used elsewhere in the script +# This should be called after mysql_check_config, but before any other functions +docker_setup_env() { + # Get config + declare -g DATADIR SOCKET + DATADIR="$(mysql_get_config 'datadir' "$@")" + SOCKET="$(mysql_get_config 'socket' "$@")" + + # Initialize values that might be stored in a file + _mariadb_file_env 'MYSQL_ROOT_HOST' '%' + _mariadb_file_env 'MYSQL_DATABASE' + _mariadb_file_env 'MYSQL_USER' + _mariadb_file_env 'MYSQL_PASSWORD' + _mariadb_file_env 'MYSQL_ROOT_PASSWORD' + # No MYSQL_ compatibility needed for new variables + file_env 'MARIADB_PASSWORD_HASH' + file_env 'MARIADB_ROOT_PASSWORD_HASH' + + # set MARIADB_ from MYSQL_ when it is unset and then make them the same value + : "${MARIADB_ALLOW_EMPTY_ROOT_PASSWORD:=${MYSQL_ALLOW_EMPTY_PASSWORD:-}}" + export MYSQL_ALLOW_EMPTY_PASSWORD="$MARIADB_ALLOW_EMPTY_ROOT_PASSWORD" MARIADB_ALLOW_EMPTY_ROOT_PASSWORD + : "${MARIADB_RANDOM_ROOT_PASSWORD:=${MYSQL_RANDOM_ROOT_PASSWORD:-}}" + export MYSQL_RANDOM_ROOT_PASSWORD="$MARIADB_RANDOM_ROOT_PASSWORD" MARIADB_RANDOM_ROOT_PASSWORD + : "${MARIADB_INITDB_SKIP_TZINFO:=${MYSQL_INITDB_SKIP_TZINFO:-}}" + export MYSQL_INITDB_SKIP_TZINFO="$MARIADB_INITDB_SKIP_TZINFO" MARIADB_INITDB_SKIP_TZINFO + + declare -g DATABASE_ALREADY_EXISTS + if [ -d "$DATADIR/mysql" ]; then + DATABASE_ALREADY_EXISTS='true' + fi +} + +# Execute the client, use via docker_process_sql to handle root password +docker_exec_client() { + # args sent in can override this db, since they will be later in the command + if [ -n "$MYSQL_DATABASE" ]; then + set -- --database="$MYSQL_DATABASE" "$@" + fi + mariadb --protocol=socket -uroot -hlocalhost --socket="${SOCKET}" "$@" +} + +# Execute sql script, passed via stdin +# usage: docker_process_sql [--dont-use-mysql-root-password] [mysql-cli-args] +# ie: docker_process_sql --database=mydb <<<'INSERT ...' +# ie: docker_process_sql --dont-use-mysql-root-password --database=mydb "${DATADIR}/${backup_db}"; then + mysql_error "Unable backup system database for upgrade from $oldfullversion." + fi + mysql_note "Backing up complete" +} + +# perform mariadb-upgrade +# backup the mysql database if this is a major upgrade +docker_mariadb_upgrade() { + if [ -z "$MARIADB_AUTO_UPGRADE" ] || + [ "$MARIADB_AUTO_UPGRADE" = 0 ]; then + mysql_note "MariaDB upgrade (mariadb-upgrade) required, but skipped due to \$MARIADB_AUTO_UPGRADE setting" + return + fi + mysql_note "Starting temporary server" + docker_temp_server_start "$@" --skip-grant-tables \ + --loose-innodb_buffer_pool_dump_at_shutdown=0 \ + --skip-slave-start + mysql_note "Temporary server started." + + docker_mariadb_backup_system + + mysql_note "Starting mariadb-upgrade" + mariadb-upgrade --upgrade-system-tables + mysql_note "Finished mariadb-upgrade" + + mysql_note "Stopping temporary server" + docker_temp_server_stop + mysql_note "Temporary server stopped" +} + +_check_if_upgrade_is_needed() { + if [ ! -f "$DATADIR"/mariadb_upgrade_info ]; then + mysql_note "MariaDB upgrade information missing, assuming required" + return 0 + fi + local mariadbVersion + mariadbVersion="$(_mariadb_version)" + IFS='.-' read -ra newversion <<<"$mariadbVersion" + IFS='.-' read -ra oldversion <"$DATADIR"/mariadb_upgrade_info || true + + if [[ ${#newversion[@]} -lt 2 ]] || [[ ${#oldversion[@]} -lt 2 ]] || + [[ ${oldversion[0]} -lt ${newversion[0]} ]] || + [[ ${oldversion[0]} -eq ${newversion[0]} && ${oldversion[1]} -lt ${newversion[1]} ]]; then + return 0 + fi + mysql_note "MariaDB upgrade not required" + return 1 +} + +# check arguments for an option that would cause mariadbd to stop +# return true if there is one +_mysql_want_help() { + local arg + for arg; do + case "$arg" in + -'?' | --help | --print-defaults | -V | --version) + return 0 + ;; + esac + done + return 1 +} + +_main() { + # if command starts with an option, prepend mariadbd + if [ "${1:0:1}" = '-' ]; then + set -- mariadbd "$@" + fi + + #ENDOFSUBSTITUTIONS + # skip setup if they aren't running mysqld or want an option that stops mysqld + if [ "$1" = 'mariadbd' ] || [ "$1" = 'mysqld' ] && ! _mysql_want_help "$@"; then + mysql_note "Entrypoint script for MariaDB Server ${MARIADB_VERSION} started." + + mysql_check_config "$@" + # Load various environment variables + docker_setup_env "$@" + docker_create_db_directories + + # If container is started as root user, restart as dedicated mysql user + if [ "$(id -u)" = "0" ]; then + mysql_note "Switching to dedicated user 'mysql'" + exec gosu mysql "${BASH_SOURCE[0]}" "$@" + fi + + # there's no database, so it needs to be initialized + if [ -z "$DATABASE_ALREADY_EXISTS" ]; then + docker_verify_minimum_env + + # check dir permissions to reduce likelihood of half-initialized database + ls /docker-entrypoint-initdb.d/ >/dev/null + + docker_init_database_dir "$@" + + mysql_note "Starting temporary server" + docker_temp_server_start "$@" + mysql_note "Temporary server started." + + docker_setup_db + docker_process_init_files /docker-entrypoint-initdb.d/* + # Wait until after /docker-entrypoint-initdb.d is performed before setting + # root@localhost password to a hash we don't know the password for. + if [ -n "${MARIADB_ROOT_PASSWORD_HASH}" ]; then + mysql_note "Setting root@localhost password hash" + docker_process_sql --dont-use-mysql-root-password --binary-mode <<-EOSQL + SET @@SESSION.SQL_LOG_BIN=0; + SET PASSWORD FOR 'root'@'localhost'= '${MARIADB_ROOT_PASSWORD_HASH}'; + EOSQL + fi + + mysql_note "Stopping temporary server" + docker_temp_server_stop + mysql_note "Temporary server stopped" + + echo + mysql_note "MariaDB init process done. Ready for start up." + echo + # MDEV-27636 mariadb_upgrade --check-if-upgrade-is-needed cannot be run offline + #elif mariadb-upgrade --check-if-upgrade-is-needed; then + elif _check_if_upgrade_is_needed; then + docker_mariadb_upgrade "$@" + fi + fi + exec "$@" +} + +# If we are sourced from elsewhere, don't perform any further actions +if ! _is_sourced; then + _main "$@" +fi diff --git a/db/mongodb.sh b/db/mongodb.sh new file mode 100644 index 0000000..6bf1cc9 --- /dev/null +++ b/db/mongodb.sh @@ -0,0 +1,434 @@ +#!/bin/bash +set -Eeuo pipefail + +if [ "${1:0:1}" = '-' ]; then + set -- mongod "$@" +fi + +originalArgOne="$1" + +# allow the container to be started with `--user` +# all mongo* commands should be dropped to the correct user +if [[ "$originalArgOne" == mongo* ]] && [ "$(id -u)" = '0' ]; then + if [ "$originalArgOne" = 'mongod' ]; then + find /data/configdb /data/db \! -user mongodb -exec chown mongodb '{}' + + fi + + # make sure we can write to stdout and stderr as "mongodb" + # (for our "initdb" code later; see "--logpath" below) + chown --dereference mongodb "/proc/$$/fd/1" "/proc/$$/fd/2" || : + # ignore errors thanks to https://github.com/docker-library/mongo/issues/149 + + exec gosu mongodb "$BASH_SOURCE" "$@" +fi + +dpkgArch="$(dpkg --print-architecture)" +case "$dpkgArch" in +amd64) # https://github.com/docker-library/mongo/issues/485#issuecomment-891991814 + if ! grep -qE '^flags.* avx( .*|$)' /proc/cpuinfo; then + { + echo + echo 'WARNING: MongoDB 5.0+ requires a CPU with AVX support, and your current system does not appear to have that!' + echo ' see https://jira.mongodb.org/browse/SERVER-54407' + echo ' see also https://www.mongodb.com/community/forums/t/mongodb-5-0-cpu-intel-g4650-compatibility/116610/2' + echo ' see also https://github.com/docker-library/mongo/issues/485#issuecomment-891991814' + echo + } >&2 + fi + ;; + +arm64) # https://github.com/docker-library/mongo/issues/485#issuecomment-970864306 + # https://en.wikichip.org/wiki/arm/armv8#ARMv8_Extensions_and_Processor_Features + # http://javathunderx.blogspot.com/2018/11/cheat-sheet-for-cpuinfo-features-on.html + if ! grep -qE '^Features.* (fphp|dcpop|sha3|sm3|sm4|asimddp|sha512|sve)( .*|$)' /proc/cpuinfo; then + { + echo + echo 'WARNING: MongoDB 5.0+ requires ARMv8.2-A or higher, and your current system does not appear to implement any of the common features for that!' + echo ' see https://jira.mongodb.org/browse/SERVER-55178' + echo ' see also https://en.wikichip.org/wiki/arm/armv8#ARMv8_Extensions_and_Processor_Features' + echo ' see also https://github.com/docker-library/mongo/issues/485#issuecomment-970864306' + echo + } >&2 + fi + ;; +esac + +# you should use numactl to start your mongod instances, including the config servers, mongos instances, and any clients. +# https://docs.mongodb.com/manual/administration/production-notes/#configuring-numa-on-linux +if [[ "$originalArgOne" == mongo* ]]; then + numa='numactl --interleave=all' + if $numa true &>/dev/null; then + set -- $numa "$@" + fi +fi + +# usage: file_env VAR [DEFAULT] +# ie: file_env 'XYZ_DB_PASSWORD' 'example' +# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of +# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature) +file_env() { + local var="$1" + local fileVar="${var}_FILE" + local def="${2:-}" + if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then + echo >&2 "error: both $var and $fileVar are set (but are exclusive)" + exit 1 + fi + local val="$def" + if [ "${!var:-}" ]; then + val="${!var}" + elif [ "${!fileVar:-}" ]; then + val="$(<"${!fileVar}")" + fi + export "$var"="$val" + unset "$fileVar" +} + +# see https://github.com/docker-library/mongo/issues/147 (mongod is picky about duplicated arguments) +_mongod_hack_have_arg() { + local checkArg="$1" + shift + local arg + for arg; do + case "$arg" in + "$checkArg" | "$checkArg"=*) + return 0 + ;; + esac + done + return 1 +} +# _mongod_hack_get_arg_val '--some-arg' "$@" +_mongod_hack_get_arg_val() { + local checkArg="$1" + shift + while [ "$#" -gt 0 ]; do + local arg="$1" + shift + case "$arg" in + "$checkArg") + echo "$1" + return 0 + ;; + "$checkArg"=*) + echo "${arg#$checkArg=}" + return 0 + ;; + esac + done + return 1 +} +declare -a mongodHackedArgs +# _mongod_hack_ensure_arg '--some-arg' "$@" +# set -- "${mongodHackedArgs[@]}" +_mongod_hack_ensure_arg() { + local ensureArg="$1" + shift + mongodHackedArgs=("$@") + if ! _mongod_hack_have_arg "$ensureArg" "$@"; then + mongodHackedArgs+=("$ensureArg") + fi +} +# _mongod_hack_ensure_no_arg '--some-unwanted-arg' "$@" +# set -- "${mongodHackedArgs[@]}" +_mongod_hack_ensure_no_arg() { + local ensureNoArg="$1" + shift + mongodHackedArgs=() + while [ "$#" -gt 0 ]; do + local arg="$1" + shift + if [ "$arg" = "$ensureNoArg" ]; then + continue + fi + mongodHackedArgs+=("$arg") + done +} +# _mongod_hack_ensure_no_arg '--some-unwanted-arg' "$@" +# set -- "${mongodHackedArgs[@]}" +_mongod_hack_ensure_no_arg_val() { + local ensureNoArg="$1" + shift + mongodHackedArgs=() + while [ "$#" -gt 0 ]; do + local arg="$1" + shift + case "$arg" in + "$ensureNoArg") + shift # also skip the value + continue + ;; + "$ensureNoArg"=*) + # value is already included + continue + ;; + esac + mongodHackedArgs+=("$arg") + done +} +# _mongod_hack_ensure_arg_val '--some-arg' 'some-val' "$@" +# set -- "${mongodHackedArgs[@]}" +_mongod_hack_ensure_arg_val() { + local ensureArg="$1" + shift + local ensureVal="$1" + shift + _mongod_hack_ensure_no_arg_val "$ensureArg" "$@" + mongodHackedArgs+=("$ensureArg" "$ensureVal") +} + +# _js_escape 'some "string" value' +_js_escape() { + jq --null-input --arg 'str' "$1" '$str' +} + +: "${TMPDIR:=/tmp}" +jsonConfigFile="$TMPDIR/docker-entrypoint-config.json" +tempConfigFile="$TMPDIR/docker-entrypoint-temp-config.json" +_parse_config() { + if [ -s "$tempConfigFile" ]; then + return 0 + fi + + local configPath + if configPath="$(_mongod_hack_get_arg_val --config "$@")" && [ -s "$configPath" ]; then + # if --config is specified, parse it into a JSON file so we can remove a few problematic keys (especially SSL-related keys) + # see https://docs.mongodb.com/manual/reference/configuration-options/ + if grep -vEm1 '^[[:space:]]*(#|$)' "$configPath" | grep -qE '^[[:space:]]*[^=:]+[[:space:]]*='; then + # if the first non-comment/non-blank line of the config file looks like "foo = ...", this is probably the 2.4 and older "ini-style config format" + # mongod tries to parse config as yaml and then falls back to ini-style parsing + # https://github.com/mongodb/mongo/blob/r6.0.3/src/mongo/util/options_parser/options_parser.cpp#L1883-L1894 + echo >&2 + echo >&2 "WARNING: it appears that '$configPath' is in the older INI-style format (replaced by YAML in MongoDB 2.6)" + echo >&2 ' This script does not parse the older INI-style format, and thus will ignore it.' + echo >&2 + return 1 + fi + if [ "$mongoShell" = 'mongo' ]; then + "$mongoShell" --norc --nodb --quiet --eval "load('/js-yaml.js'); printjson(jsyaml.load(cat($(_js_escape "$configPath"))))" >"$jsonConfigFile" + else + # https://www.mongodb.com/docs/manual/reference/method/js-native/#std-label-native-in-mongosh + "$mongoShell" --norc --nodb --quiet --eval "load('/js-yaml.js'); JSON.stringify(jsyaml.load(fs.readFileSync($(_js_escape "$configPath"), 'utf8')))" >"$jsonConfigFile" + fi + if [ "$(head -c1 "$jsonConfigFile")" != '{' ] || [ "$(tail -c2 "$jsonConfigFile")" != '}' ]; then + # if the file doesn't start with "{" and end with "}", it's *probably* an error ("uncaught exception: YAMLException: foo" for example), so we should print it out + echo >&2 'error: unexpected "js-yaml.js" output while parsing config:' + cat >&2 "$jsonConfigFile" + exit 1 + fi + jq 'del(.systemLog, .processManagement, .net, .security, .replication)' "$jsonConfigFile" >"$tempConfigFile" + return 0 + fi + + return 1 +} +dbPath= +_dbPath() { + if [ -n "$dbPath" ]; then + echo "$dbPath" + return + fi + + if ! dbPath="$(_mongod_hack_get_arg_val --dbpath "$@")"; then + if _parse_config "$@"; then + dbPath="$(jq -r '.storage.dbPath // empty' "$jsonConfigFile")" + fi + fi + + if [ -z "$dbPath" ]; then + if _mongod_hack_have_arg --configsvr "$@" || { + _parse_config "$@" && + clusterRole="$(jq -r '.sharding.clusterRole // empty' "$jsonConfigFile")" && + [ "$clusterRole" = 'configsvr' ] + }; then + # if running as config server, then the default dbpath is /data/configdb + # https://docs.mongodb.com/manual/reference/program/mongod/#cmdoption-mongod-configsvr + dbPath=/data/configdb + fi + fi + + : "${dbPath:=/data/db}" + + echo "$dbPath" +} + +if [ "$originalArgOne" = 'mongod' ]; then + file_env 'MONGO_INITDB_ROOT_USERNAME' + file_env 'MONGO_INITDB_ROOT_PASSWORD' + + mongoShell='mongo' + if ! command -v "$mongoShell" >/dev/null; then + mongoShell='mongosh' + fi + + # pre-check a few factors to see if it's even worth bothering with initdb + shouldPerformInitdb= + if [ "$MONGO_INITDB_ROOT_USERNAME" ] && [ "$MONGO_INITDB_ROOT_PASSWORD" ]; then + # if we have a username/password, let's set "--auth" + _mongod_hack_ensure_arg '--auth' "$@" + set -- "${mongodHackedArgs[@]}" + shouldPerformInitdb='true' + elif [ "$MONGO_INITDB_ROOT_USERNAME" ] || [ "$MONGO_INITDB_ROOT_PASSWORD" ]; then + cat >&2 <<-'EOF' + + error: missing 'MONGO_INITDB_ROOT_USERNAME' or 'MONGO_INITDB_ROOT_PASSWORD' + both must be specified for a user to be created + + EOF + exit 1 + fi + + if [ -z "$shouldPerformInitdb" ]; then + # if we've got any /docker-entrypoint-initdb.d/* files to parse later, we should initdb + for f in /docker-entrypoint-initdb.d/*; do + case "$f" in + *.sh | *.js) # this should match the set of files we check for below + shouldPerformInitdb="$f" + break + ;; + esac + done + fi + + # check for a few known paths (to determine whether we've already initialized and should thus skip our initdb scripts) + if [ -n "$shouldPerformInitdb" ]; then + dbPath="$(_dbPath "$@")" + for path in \ + "$dbPath/WiredTiger" \ + "$dbPath/journal" \ + "$dbPath/local.0" \ + "$dbPath/storage.bson"; do + if [ -e "$path" ]; then + shouldPerformInitdb= + break + fi + done + fi + + if [ -n "$shouldPerformInitdb" ]; then + mongodHackedArgs=("$@") + if _parse_config "$@"; then + _mongod_hack_ensure_arg_val --config "$tempConfigFile" "${mongodHackedArgs[@]}" + fi + _mongod_hack_ensure_arg_val --bind_ip 127.0.0.1 "${mongodHackedArgs[@]}" + _mongod_hack_ensure_arg_val --port 27017 "${mongodHackedArgs[@]}" + _mongod_hack_ensure_no_arg --bind_ip_all "${mongodHackedArgs[@]}" + + # remove "--auth" and "--replSet" for our initial startup (see https://docs.mongodb.com/manual/tutorial/enable-authentication/#start-mongodb-without-access-control) + # https://github.com/docker-library/mongo/issues/211 + _mongod_hack_ensure_no_arg --auth "${mongodHackedArgs[@]}" + # "keyFile implies security.authorization" + # https://docs.mongodb.com/manual/reference/configuration-options/#mongodb-setting-security.keyFile + _mongod_hack_ensure_no_arg_val --keyFile "${mongodHackedArgs[@]}" + if [ "$MONGO_INITDB_ROOT_USERNAME" ] && [ "$MONGO_INITDB_ROOT_PASSWORD" ]; then + _mongod_hack_ensure_no_arg_val --replSet "${mongodHackedArgs[@]}" + fi + + # "BadValue: need sslPEMKeyFile when SSL is enabled" vs "BadValue: need to enable SSL via the sslMode flag when using SSL configuration parameters" + tlsMode='disabled' + if _mongod_hack_have_arg '--tlsCertificateKeyFile' "$@"; then + tlsMode='allowTLS' + fi + _mongod_hack_ensure_arg_val --tlsMode "$tlsMode" "${mongodHackedArgs[@]}" + + if stat "/proc/$$/fd/1" >/dev/null && [ -w "/proc/$$/fd/1" ]; then + # https://github.com/mongodb/mongo/blob/38c0eb538d0fd390c6cb9ce9ae9894153f6e8ef5/src/mongo/db/initialize_server_global_state.cpp#L237-L251 + # https://github.com/docker-library/mongo/issues/164#issuecomment-293965668 + _mongod_hack_ensure_arg_val --logpath "/proc/$$/fd/1" "${mongodHackedArgs[@]}" + else + initdbLogPath="$(_dbPath "$@")/docker-initdb.log" + echo >&2 "warning: initdb logs cannot write to '/proc/$$/fd/1', so they are in '$initdbLogPath' instead" + _mongod_hack_ensure_arg_val --logpath "$initdbLogPath" "${mongodHackedArgs[@]}" + fi + _mongod_hack_ensure_arg --logappend "${mongodHackedArgs[@]}" + + pidfile="$TMPDIR/docker-entrypoint-temp-mongod.pid" + rm -f "$pidfile" + _mongod_hack_ensure_arg_val --pidfilepath "$pidfile" "${mongodHackedArgs[@]}" + + "${mongodHackedArgs[@]}" --fork + + mongo=("$mongoShell" --host 127.0.0.1 --port 27017 --quiet) + + # check to see that our "mongod" actually did start up (catches "--help", "--version", MongoDB 3.2 being silly, slow prealloc, etc) + # https://jira.mongodb.org/browse/SERVER-16292 + tries=30 + while true; do + if ! { [ -s "$pidfile" ] && ps "$(<"$pidfile")" &>/dev/null; }; then + # bail ASAP if "mongod" isn't even running + echo >&2 + echo >&2 "error: $originalArgOne does not appear to have stayed running -- perhaps it had an error?" + echo >&2 + exit 1 + fi + if "${mongo[@]}" 'admin' --eval 'quit(0)' &>/dev/null; then + # success! + break + fi + ((tries--)) + if [ "$tries" -le 0 ]; then + echo >&2 + echo >&2 "error: $originalArgOne does not appear to have accepted connections quickly enough -- perhaps it had an error?" + echo >&2 + exit 1 + fi + sleep 1 + done + + if [ "$MONGO_INITDB_ROOT_USERNAME" ] && [ "$MONGO_INITDB_ROOT_PASSWORD" ]; then + rootAuthDatabase='admin' + + "${mongo[@]}" "$rootAuthDatabase" <<-EOJS + db.createUser({ + user: $(_js_escape "$MONGO_INITDB_ROOT_USERNAME"), + pwd: $(_js_escape "$MONGO_INITDB_ROOT_PASSWORD"), + roles: [ { role: 'root', db: $(_js_escape "$rootAuthDatabase") } ] + }) + EOJS + fi + + export MONGO_INITDB_DATABASE="${MONGO_INITDB_DATABASE:-test}" + + echo + for f in /docker-entrypoint-initdb.d/*; do + case "$f" in + *.sh) + echo "$0: running $f" + . "$f" + ;; + *.js) + echo "$0: running $f" + "${mongo[@]}" "$MONGO_INITDB_DATABASE" "$f" + echo + ;; + *) echo "$0: ignoring $f" ;; + esac + echo + done + + "${mongodHackedArgs[@]}" --shutdown + rm -f "$pidfile" + + echo + echo 'MongoDB init process complete; ready for start up.' + echo + fi + + # MongoDB 3.6+ defaults to localhost-only binding + haveBindIp= + if _mongod_hack_have_arg --bind_ip "$@" || _mongod_hack_have_arg --bind_ip_all "$@"; then + haveBindIp=1 + elif _parse_config "$@" && jq --exit-status '.net.bindIp // .net.bindIpAll' "$jsonConfigFile" >/dev/null; then + haveBindIp=1 + fi + if [ -z "$haveBindIp" ]; then + # so if no "--bind_ip" is specified, let's add "--bind_ip_all" + set -- "$@" --bind_ip_all + fi + + unset "${!MONGO_INITDB_@}" +fi + +rm -f "$jsonConfigFile" "$tempConfigFile" + +exec "$@" diff --git a/db/postgresql.sh b/db/postgresql.sh new file mode 100644 index 0000000..ff4b969 --- /dev/null +++ b/db/postgresql.sh @@ -0,0 +1,372 @@ +#!/usr/bin/env bash +set -Eeo pipefail +# TODO swap to -Eeuo pipefail above (after handling all potentially-unset variables) +PGDATA="$DATABASE_DIR_PGSQL" +POSTGRES_USER="$DATABASE_USER_ROOT" +POSTGRES_PASSWORD="$DATABASE_PASS_ROOT" +# usage: file_env VAR [DEFAULT] +# ie: file_env 'XYZ_DB_PASSWORD' 'example' +# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of +# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature) +file_env() { + local var="$1" + local fileVar="${var}_FILE" + local def="${2:-}" + if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then + printf >&2 'error: both %s and %s are set (but are exclusive)\n' "$var" "$fileVar" + exit 1 + fi + local val="$def" + if [ "${!var:-}" ]; then + val="${!var}" + elif [ "${!fileVar:-}" ]; then + val="$(<"${!fileVar}")" + fi + export "$var"="$val" + unset "$fileVar" +} + +# check to see if this file is being run or sourced from another script +_is_sourced() { + # https://unix.stackexchange.com/a/215279 + [ "${#FUNCNAME[@]}" -ge 2 ] && + [ "${FUNCNAME[0]}" = '_is_sourced' ] && + [ "${FUNCNAME[1]}" = 'source' ] +} + +# used to create initial postgres directories and if run as root, ensure ownership to the "postgres" user +docker_create_db_directories() { + local user + user="$(id -u)" + + mkdir -p "$PGDATA" + # ignore failure since there are cases where we can't chmod (and PostgreSQL might fail later anyhow - it's picky about permissions of this directory) + chmod 700 "$PGDATA" || : + + # ignore failure since it will be fine when using the image provided directory; see also https://github.com/docker-library/postgres/pull/289 + mkdir -p /var/run/postgresql || : + chmod 775 /var/run/postgresql || : + + # Create the transaction log directory before initdb is run so the directory is owned by the correct user + if [ -n "${POSTGRES_INITDB_WALDIR:-}" ]; then + mkdir -p "$POSTGRES_INITDB_WALDIR" + if [ "$user" = '0' ]; then + find "$POSTGRES_INITDB_WALDIR" \! -user postgres -exec chown postgres '{}' + + fi + chmod 700 "$POSTGRES_INITDB_WALDIR" + fi + + # allow the container to be started with `--user` + if [ "$user" = '0' ]; then + find "$PGDATA" \! -user postgres -exec chown postgres '{}' + + find /var/run/postgresql \! -user postgres -exec chown postgres '{}' + + fi +} + +# initialize empty PGDATA directory with new database via 'initdb' +# arguments to `initdb` can be passed via POSTGRES_INITDB_ARGS or as arguments to this function +# `initdb` automatically creates the "postgres", "template0", and "template1" dbnames +# this is also where the database user is created, specified by `POSTGRES_USER` env +docker_init_database_dir() { + # "initdb" is particular about the current user existing in "/etc/passwd", so we use "nss_wrapper" to fake that if necessary + # see https://github.com/docker-library/postgres/pull/253, https://github.com/docker-library/postgres/issues/359, https://cwrap.org/nss_wrapper.html + local uid + uid="$(id -u)" + if ! getent passwd "$uid" &>/dev/null; then + # see if we can find a suitable "libnss_wrapper.so" (https://salsa.debian.org/sssd-team/nss-wrapper/-/commit/b9925a653a54e24d09d9b498a2d913729f7abb15) + local wrapper + for wrapper in {/usr,}/lib{/*,}/libnss_wrapper.so; do + if [ -s "$wrapper" ]; then + NSS_WRAPPER_PASSWD="$(mktemp)" + NSS_WRAPPER_GROUP="$(mktemp)" + export LD_PRELOAD="$wrapper" NSS_WRAPPER_PASSWD NSS_WRAPPER_GROUP + local gid + gid="$(id -g)" + printf 'postgres:x:%s:%s:PostgreSQL:%s:/bin/false\n' "$uid" "$gid" "$PGDATA" >"$NSS_WRAPPER_PASSWD" + printf 'postgres:x:%s:\n' "$gid" >"$NSS_WRAPPER_GROUP" + break + fi + done + fi + + if [ -n "${POSTGRES_INITDB_WALDIR:-}" ]; then + set -- --waldir "$POSTGRES_INITDB_WALDIR" "$@" + fi + + # --pwfile refuses to handle a properly-empty file (hence the "\n"): https://github.com/docker-library/postgres/issues/1025 + eval 'initdb --username="$POSTGRES_USER" --pwfile=<(printf "%s\n" "$POSTGRES_PASSWORD") '"$POSTGRES_INITDB_ARGS"' "$@"' + + # unset/cleanup "nss_wrapper" bits + if [[ "${LD_PRELOAD:-}" == */libnss_wrapper.so ]]; then + rm -f "$NSS_WRAPPER_PASSWD" "$NSS_WRAPPER_GROUP" + unset LD_PRELOAD NSS_WRAPPER_PASSWD NSS_WRAPPER_GROUP + fi +} + +# print large warning if POSTGRES_PASSWORD is long +# error if both POSTGRES_PASSWORD is empty and POSTGRES_HOST_AUTH_METHOD is not 'trust' +# print large warning if POSTGRES_HOST_AUTH_METHOD is set to 'trust' +# assumes database is not set up, ie: [ -z "$DATABASE_ALREADY_EXISTS" ] +docker_verify_minimum_env() { + # check password first so we can output the warning before postgres + # messes it up + if [ "${#POSTGRES_PASSWORD}" -ge 100 ]; then + cat >&2 <<-'EOWARN' + + WARNING: The supplied POSTGRES_PASSWORD is 100+ characters. + + This will not work if used via PGPASSWORD with "psql". + + https://www.postgresql.org/message-id/flat/E1Rqxp2-0004Qt-PL%40wrigleys.postgresql.org (BUG #6412) + https://github.com/docker-library/postgres/issues/507 + + EOWARN + fi + if [ -z "$POSTGRES_PASSWORD" ] && [ 'trust' != "$POSTGRES_HOST_AUTH_METHOD" ]; then + # The - option suppresses leading tabs but *not* spaces. :) + cat >&2 <<-'EOE' + Error: Database is uninitialized and superuser password is not specified. + You must specify POSTGRES_PASSWORD to a non-empty value for the + superuser. For example, "-e POSTGRES_PASSWORD=password" on "docker run". + + You may also use "POSTGRES_HOST_AUTH_METHOD=trust" to allow all + connections without a password. This is *not* recommended. + + See PostgreSQL documentation about "trust": + https://www.postgresql.org/docs/current/auth-trust.html + EOE + exit 1 + fi + if [ 'trust' = "$POSTGRES_HOST_AUTH_METHOD" ]; then + cat >&2 <<-'EOWARN' + ******************************************************************************** + WARNING: POSTGRES_HOST_AUTH_METHOD has been set to "trust". This will allow + anyone with access to the Postgres port to access your database without + a password, even if POSTGRES_PASSWORD is set. See PostgreSQL + documentation about "trust": + https://www.postgresql.org/docs/current/auth-trust.html + In Docker's default configuration, this is effectively any other + container on the same system. + + It is not recommended to use POSTGRES_HOST_AUTH_METHOD=trust. Replace + it with "-e POSTGRES_PASSWORD=password" instead to set a password in + "docker run". + ******************************************************************************** + EOWARN + fi +} + +# usage: docker_process_init_files [file [file [...]]] +# ie: docker_process_init_files /always-initdb.d/* +# process initializer files, based on file extensions and permissions +docker_process_init_files() { + # psql here for backwards compatibility "${psql[@]}" + psql=(docker_process_sql) + + printf '\n' + local f + for f; do + case "$f" in + *.sh) + # https://github.com/docker-library/postgres/issues/450#issuecomment-393167936 + # https://github.com/docker-library/postgres/pull/452 + if [ -x "$f" ]; then + printf '%s: running %s\n' "$0" "$f" + "$f" + else + printf '%s: sourcing %s\n' "$0" "$f" + . "$f" + fi + ;; + *.sql) + printf '%s: running %s\n' "$0" "$f" + docker_process_sql -f "$f" + printf '\n' + ;; + *.sql.gz) + printf '%s: running %s\n' "$0" "$f" + gunzip -c "$f" | docker_process_sql + printf '\n' + ;; + *.sql.xz) + printf '%s: running %s\n' "$0" "$f" + xzcat "$f" | docker_process_sql + printf '\n' + ;; + *.sql.zst) + printf '%s: running %s\n' "$0" "$f" + zstd -dc "$f" | docker_process_sql + printf '\n' + ;; + *) printf '%s: ignoring %s\n' "$0" "$f" ;; + esac + printf '\n' + done +} + +# Execute sql script, passed via stdin (or -f flag of pqsl) +# usage: docker_process_sql [psql-cli-args] +# ie: docker_process_sql --dbname=mydb <<<'INSERT ...' +# ie: docker_process_sql -f my-file.sql +# ie: docker_process_sql >"$PGDATA/pg_hba.conf" +} + +# start socket-only postgresql server for setting up or running scripts +# all arguments will be passed along as arguments to `postgres` (via pg_ctl) +docker_temp_server_start() { + if [ "$1" = 'postgres' ]; then + shift + fi + + # internal start of server in order to allow setup using psql client + # does not listen on external TCP/IP and waits until start finishes + set -- "$@" -c listen_addresses='' -p "${PGPORT:-5432}" + + PGUSER="${PGUSER:-$POSTGRES_USER}" \ + pg_ctl -D "$PGDATA" \ + -o "$(printf '%q ' "$@")" \ + -w start +} + +# stop postgresql server after done setting up user and running scripts +docker_temp_server_stop() { + PGUSER="${PGUSER:-postgres}" \ + pg_ctl -D "$PGDATA" -m fast -w stop +} + +# check arguments for an option that would cause postgres to stop +# return true if there is one +_pg_want_help() { + local arg + for arg; do + case "$arg" in + # postgres --help | grep 'then exit' + # leaving out -C on purpose since it always fails and is unhelpful: + # postgres: could not access the server configuration file "/var/lib/postgresql/data/postgresql.conf": No such file or directory + -'?' | --help | --describe-config | -V | --version) + return 0 + ;; + esac + done + return 1 +} + +_main() { + # if first arg looks like a flag, assume we want to run postgres server + if [ "${1:0:1}" = '-' ]; then + set -- postgres "$@" + fi + + if [ "$1" = 'postgres' ] && ! _pg_want_help "$@"; then + docker_setup_env + # setup data directories and permissions (when run as root) + docker_create_db_directories + if [ "$(id -u)" = '0' ]; then + # then restart script as postgres user + exec su-exec postgres "$BASH_SOURCE" "$@" + fi + + # only run initialization on an empty data directory + if [ -z "$DATABASE_ALREADY_EXISTS" ]; then + docker_verify_minimum_env + + # check dir permissions to reduce likelihood of half-initialized database + ls /docker-entrypoint-initdb.d/ >/dev/null + + docker_init_database_dir + pg_setup_hba_conf "$@" + + # PGPASSWORD is required for psql when authentication is required for 'local' connections via pg_hba.conf and is otherwise harmless + # e.g. when '--auth=md5' or '--auth-local=md5' is used in POSTGRES_INITDB_ARGS + export PGPASSWORD="${PGPASSWORD:-$POSTGRES_PASSWORD}" + docker_temp_server_start "$@" + + docker_setup_db + docker_process_init_files /docker-entrypoint-initdb.d/* + + docker_temp_server_stop + unset PGPASSWORD + + cat <<-'EOM' + + PostgreSQL init process complete; ready for start up. + + EOM + else + cat <<-'EOM' + + PostgreSQL Database directory appears to contain a database; Skipping initialization + + EOM + fi + fi + + exec "$@" +} + +if ! _is_sourced; then + _main "$@" +fi diff --git a/dockerfile.sh b/dockerfile.sh index f61a487..46071fa 100755 --- a/dockerfile.sh +++ b/dockerfile.sh @@ -54,6 +54,7 @@ export OPEN_EDITOR="${OPEN_EDITOR:-yes}" CREATE_CMD="${CREATE_CMD:-gen-dockerfile}" DATE="$(date +'%Y%m%d%H%M')" SET_DIR="$PWD" +exitCode=0 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if [ $# -eq 0 ]; then REPO_DIRS="$(ls -A "$DOCKER_GIT_DIR")" @@ -75,10 +76,17 @@ if [ "$CREATE_CMD" = "gen-dockerfile" ] || [ "$CREATE_CMD" = "file" ]; then DIR_NEW="$(realpath "$DOCKER_DEV_DIR/$d" 2>/dev/null)" DIR_OLD="$(realpath "$DOCKER_GIT_DIR/$d" 2>/dev/null)" if [ ! -d "$DIR_NEW" ]; then - backupapp --once "$d" "$DIR_OLD" + if [ -d "$DIR_OLD" ]; then + echo "Backing up $DIR_OLD to $DIR_OLD.$$" + backupapp --once "$d" "$DIR_OLD" &>/dev/null + __cp "$DIR_OLD" "$DIR_OLD.$$" + fi mkdir -p "$DIR_NEW" "$DIR_OLD" mkdir -p "$DIR_NEW/rootfs/usr/local/share/template-files/data" mkdir -p "$DIR_NEW/rootfs/usr/local/share/template-files/config" + gitignore "$DIR_NEW" dirignore,default --automated + eval $CREATE_CMD --nogit --dir "$DIR_NEW" --template $TEMPLATE + sCode=$? if [ -f "$DIR_OLD/Dockerfile" ] && [ -f "$DIR_NEW/Dockerfile.bak" ]; then echo "Moving Dockerfile to Docker.bak" __mv "$DIR_OLD/Dockerfile.bak" "$DIR_NEW/Dockerfile.$DATE.bak" @@ -94,9 +102,6 @@ if [ "$CREATE_CMD" = "gen-dockerfile" ] || [ "$CREATE_CMD" = "file" ]; then echo "Moving rootfs to rootfs.bak" __mv "$DIR_OLD/rootfs" "$DIR_NEW/rootfs.bak" fi - gitignore "$DIR_NEW" dirignore,default --automated - eval $CREATE_CMD --nogit --dir "$DIR_NEW" --template $TEMPLATE - sCode=$? if [ $sCode -eq 0 ]; then for otherDockerfile in "$DIR_OLD/Dockerfile"*; do if [ -f "$otherDockerfile" ]; then @@ -117,7 +122,7 @@ if [ "$CREATE_CMD" = "gen-dockerfile" ] || [ "$CREATE_CMD" = "file" ]; then fi if [ -d "$DIR_NEW/rootfs.bak/usr/local/share/template-files" ]; then echo "Moving template files" - __cp "$DIR_OLD/rootfs.bak/usr/local/share/template-files/." "$DIR_NEW/rootfs/usr/local/share/template-files/" + __cp "$DIR_NEW/rootfs.bak/usr/local/share/template-files/." "$DIR_NEW/rootfs/usr/local/share/template-files/" fi [ -f "$DIR_NEW/Dockerfile" ] || { echo "Failed to init $DIR_NEW" && exit 2; } [ -d "$DIR_OLD/.git" ] && echo "Moving the git directory" && __cp "$DIR_OLD/.git" "$DIR_NEW/.git" @@ -126,8 +131,9 @@ if [ "$CREATE_CMD" = "gen-dockerfile" ] || [ "$CREATE_CMD" = "file" ]; then code -nw "$DIR_NEW" && true && sleep 3 || false sCode="$?" fi + [ -d "$DIR_OLD" ] && __rmw "$DIR_OLD" if [ $sCode -eq 0 ] && [ "$RECREATE" != "no" ]; then - __rmw "$DIR_OLD" && mkdir -p "$DIR_OLD" && __cp "$DIR_NEW/." "$DIR_OLD/" && __rm "$DIR_NEW" + mkdir -p "$DIR_OLD" && __cp "$DIR_NEW/." "$DIR_OLD/" && __rm "$DIR_NEW" if [ "$OPEN_EDITOR" = "yes" ] && cd "$DIR_OLD" && [ -d "$DIR_OLD/.git" ]; then gitcommit "$DIR_OLD" all || exit 1 gitcommit "$DIR_OLD" all || exit 1 @@ -135,13 +141,15 @@ if [ "$CREATE_CMD" = "gen-dockerfile" ] || [ "$CREATE_CMD" = "file" ]; then fi else echo 'Something went wrong' - exit 1 + exitCode=$((1 + exitCode)) fi sleep 3 else echo "The dir exists: $DIR_NEW" fi + printf '%\s\n\n' "# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -" done + exit $exitCode elif [ "$CREATE_CMD" = "gen-dockermgr" ] || [ "$CREATE_CMD" = "mgr" ]; then DOCKER_GIT_DIR="$HOME/Projects/github/dockermgr" DOCKER_DEV_DIR="$HOME/.local/share/cdd/projects/dockermgr" @@ -152,7 +160,10 @@ elif [ "$CREATE_CMD" = "gen-dockermgr" ] || [ "$CREATE_CMD" = "mgr" ]; then DIR_NEW="$(realpath "$DOCKER_DEV_DIR/$d" 2>/dev/null)" DIR_OLD="$(realpath "$DOCKER_GIT_DIR/$d" 2>/dev/null)" if [ ! -d "$DIR_NEW" ]; then - backupapp --once "$d" "$DIR_OLD" + if [ -d "$DIR_OLD" ]; then + backupapp --once "$d" "$DIR_OLD" &>/dev/null + __cp "$DIR_OLD" "$DIR_OLD.$$" + fi mkdir -p "$DIR_NEW" "$DIR_OLD" "$DIR_NEW/rootfs/data" "$DIR_NEW/rootfs/config" if [ -f "$DIR_OLD/install.sh" ] && [ -f "$DIR_OLD/install.sh.bak" ]; then echo "Moving install.sh to install.sh.bak" @@ -189,20 +200,22 @@ elif [ "$CREATE_CMD" = "gen-dockermgr" ] || [ "$CREATE_CMD" = "mgr" ]; then code -nw "$DIR_NEW" && true && sleep 3 || false sCode="$?" fi + [ -d "$DIR_OLD" ] && __rmw "$DIR_OLD/" if [ $sCode -eq 0 ] && [ "$RECREATE" != "no" ]; then - __rmw "$DIR_OLD/" && __cp "$DIR_NEW/." "$DIR_OLD/" && __rm "$DIR_NEW" + __cp "$DIR_NEW/." "$DIR_OLD/" && __rm "$DIR_NEW" if [ "$OPEN_EDITOR" = "yes" ] && cd "$DIR_OLD" && [ -d "$DIR_OLD/.git" ]; then gitcommit "$DIR_OLD" all || exit 1 gitcommit "$DIR_OLD" all || exit 1 fi else echo 'Something went wrong' - exit 1 + exitCode=$((1 + exitCode)) fi sleep 3 else echo "The dir exists: $DIR_NEW" fi + printf '%\s\n\n' "# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -" done elif [ "$CREATE_CMD" = "gen-dockerboth" ] || [ "$CREATE_CMD" = "gen-dockerall" ]; then [ "$DOCKERFILE_RUN_BOTH" = "true" ] && exit 1 diff --git a/version.txt b/version.txt index d2a7fd8..a0d24eb 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -202303031839-git +202303032113-git