#!/bin/bash # Copyright VMware, Inc. # SPDX-License-Identifier: APACHE-2.0 # # Bitnami MongoDB library # shellcheck disable=SC1090,SC1091 # Load Generic Libraries . /opt/bitnami/scripts/libfile.sh . /opt/bitnami/scripts/liblog.sh . /opt/bitnami/scripts/libservice.sh . /opt/bitnami/scripts/libvalidations.sh . /opt/bitnami/scripts/libos.sh . /opt/bitnami/scripts/libfs.sh . /opt/bitnami/scripts/libnet.sh ######################## # Return field separator to use in lists. One of comma or semi-colon, comma # being preferred. # Globals: # None # Arguments: # A (list) of fields # Returns: # The separator used within that list ######################### mongodb_field_separator() { if printf %s\\n "$1" | grep -q ','; then echo ',' elif printf %s\\n "$1" | grep -q ';'; then echo ';' fi } ######################## # Initialise the arrays databases, usernames and passwords to contain the # fields from their respective environment variables. # Globals: # MONGODB_EXTRA_DATABASES, MONGODB_EXTRA_USERNAMES, MONGODB_EXTRA_PASSWORDS # MONGODB_DATABASE, MONGODB_USERNAME, MONGODB_PASSWORD # Arguments: # $1 - single: initialise based on MONGODB_DATABASE, MONGODB_USERNAME, MONGODB_PASSWORD # $1 - extra: initialise based on MONGODB_EXTRA_DATABASES, MONGODB_EXTRA_USERNAMES, MONGODB_EXTRA_PASSWORDS # $1 - all (or empty): initalise as both of the above # Returns: # None ######################### mongodb_auth() { case "${1:-all}" in extra) local -a databases_extra local -a usernames_extra local -a passwords_extra # Start by filling in locally scoped databases, usernames and # passwords arrays with the content of the _EXTRA_ environment # variables. IFS="$(mongodb_field_separator "$MONGODB_EXTRA_DATABASES")" read -r -a databases_extra <<<"$MONGODB_EXTRA_DATABASES" IFS="$(mongodb_field_separator "$MONGODB_EXTRA_USERNAMES")" read -r -a usernames_extra <<<"$MONGODB_EXTRA_USERNAMES" IFS="$(mongodb_field_separator "$MONGODB_EXTRA_PASSWORDS")" read -r -a passwords_extra <<<"$MONGODB_EXTRA_PASSWORDS" # Force missing empty passwords/database names (occurs when # MONGODB_EXTRA_PASSWORDS/DATABASES ends with a separator, e.g. a # comma or semi-colon), then copy into the databases, usernames and # passwords arrays (global). for ((i = 0; i < ${#usernames_extra[@]}; i++)); do if [[ -z "${passwords_extra[i]:-}" ]]; then passwords_extra[i]="" fi if [[ -z "${databases_extra[i]:-}" ]]; then databases_extra[i]="" fi databases+=("${databases_extra[i]}") usernames+=("${usernames_extra[i]}") passwords+=("${passwords_extra[i]}") done ;; single) # Add the content of the "regular" environment variables to the arrays databases+=("$MONGODB_DATABASE") usernames+=("$MONGODB_USERNAME") passwords+=("$MONGODB_PASSWORD") ;; all) # Perform the following in this order to respect the priority of the # environment variables. mongodb_auth single mongodb_auth extra ;; esac } ######################## # Validate settings in MONGODB_* env. variables # Globals: # MONGODB_* # Arguments: # None # Returns: # None ######################### mongodb_validate() { info "Validating settings in MONGODB_* env vars..." local error_message="" local -r replicaset_error_message="In order to configure MongoDB replica set authentication you \ need to provide the MONGODB_REPLICA_SET_KEY on every node, specify MONGODB_ROOT_PASSWORD \ in the primary node and MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD in the rest of nodes" local error_code=0 local usernames databases passwords # Auxiliary functions print_validation_error() { error "$1" error_code=1 } check_yes_no_value() { if ! is_yes_no_value "${!1}" && ! is_true_false_value "${!1}"; then print_validation_error "The allowed values for ${1} are: yes no" fi } if [[ -n "$MONGODB_REPLICA_SET_MODE" ]]; then if [[ "$MONGODB_REPLICA_SET_MODE" =~ ^(secondary|arbiter|hidden) ]]; then if [[ -z "$MONGODB_INITIAL_PRIMARY_HOST" ]]; then error_message="In order to configure MongoDB as a secondary or arbiter node \ you need to provide the MONGODB_INITIAL_PRIMARY_HOST env var" print_validation_error "$error_message" fi if { [[ -n "$MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD" ]] && [[ -z "$MONGODB_REPLICA_SET_KEY" ]]; } || { [[ -z "$MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD" ]] && [[ -n "$MONGODB_REPLICA_SET_KEY" ]]; }; then print_validation_error "$replicaset_error_message" fi if [[ -n "$MONGODB_ROOT_PASSWORD" ]]; then error_message="MONGODB_ROOT_PASSWORD shouldn't be set on a 'non-primary' node" print_validation_error "$error_message" fi elif [[ "$MONGODB_REPLICA_SET_MODE" = "primary" ]]; then if { [[ -n "$MONGODB_ROOT_PASSWORD" ]] && [[ -z "$MONGODB_REPLICA_SET_KEY" ]]; } || { [[ -z "$MONGODB_ROOT_PASSWORD" ]] && [[ -n "$MONGODB_REPLICA_SET_KEY" ]]; }; then print_validation_error "$replicaset_error_message" fi if [[ -n "$MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD" ]]; then error_message="MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD shouldn't be set on a 'primary' node" print_validation_error "$error_message" fi if [[ -z "$MONGODB_ROOT_PASSWORD" ]] && ! is_boolean_yes "$ALLOW_EMPTY_PASSWORD"; then error_message="The MONGODB_ROOT_PASSWORD environment variable is empty or not set. \ Set the environment variable ALLOW_EMPTY_PASSWORD=yes to allow the container to be started with blank passwords. \ This is only recommended for development." print_validation_error "$error_message" fi else error_message="You set the environment variable MONGODB_REPLICA_SET_MODE with an invalid value. \ Available options are 'primary/secondary/arbiter/hidden'" print_validation_error "$error_message" fi fi check_yes_no_value "MONGODB_ENABLE_MAJORITY_READ" [[ "$(mongodb_get_version)" =~ ^5\..\. ]] && ! is_boolean_yes "$MONGODB_ENABLE_MAJORITY_READ" && warn "MONGODB_ENABLE_MAJORITY_READ=${MONGODB_ENABLE_MAJORITY_READ} Will be ignored in MongoDB 5.0" if [[ -n "$MONGODB_REPLICA_SET_KEY" ]] && ((${#MONGODB_REPLICA_SET_KEY} < 5)); then error_message="MONGODB_REPLICA_SET_KEY must be, at least, 5 characters long!" print_validation_error "$error_message" fi if [[ -n "$MONGODB_EXTRA_USERNAMES" ]]; then # Capture list of extra (only!) users, passwords and databases in the # usernames, passwords and databases arrays. mongodb_auth extra # Verify there as many usernames as passwords if [[ "${#usernames[@]}" -ne "${#passwords[@]}" ]]; then print_validation_error "Specify the same number of passwords on MONGODB_EXTRA_PASSWORDS as the number of users in MONGODB_EXTRA_USERNAMES" fi # When we have a list of databases, there should be as many databases as # users (thus as passwords). if [[ -n "$MONGODB_EXTRA_DATABASES" ]] && [[ "${#usernames[@]}" -ne "${#databases[@]}" ]]; then print_validation_error "Specify the same number of users on MONGODB_EXTRA_USERNAMES as the number of databases in MONGODB_EXTRA_DATABASES" fi # When the list of database is empty, then all users will be added to # default database. if [[ -z "$MONGODB_EXTRA_DATABASES" ]]; then warn "All users specified in MONGODB_EXTRA_USERNAMES will be added to the default database called 'test'" fi fi if is_boolean_yes "$ALLOW_EMPTY_PASSWORD"; then warn "You set the environment variable ALLOW_EMPTY_PASSWORD=${ALLOW_EMPTY_PASSWORD}. For safety reasons, do not use this flag in a production environment." elif { [[ -n "$MONGODB_EXTRA_USERNAMES" ]] || [[ -n "$MONGODB_USERNAME" ]]; } && [[ -z "$MONGODB_ROOT_PASSWORD" ]]; then # Authorization is turned on as soon as a set of users or a root # password are given. If we have a set of users, but an empty root # password, validation should fail unless ALLOW_EMPTY_PASSWORD is turned # on. error_message="The MONGODB_ROOT_PASSWORD environment variable is empty or not set. Set the environment variable ALLOW_EMPTY_PASSWORD=yes to allow the container to be started with a blank root password. This is only recommended for development." print_validation_error "$error_message" fi # Warn for users with empty passwords, as these won't be created. Maybe # should we just end with an error here instead? if [[ -n "$MONGODB_EXTRA_USERNAMES" ]]; then # Here we can access the arrays usernames and passwordsa, as these have # been initialised earlier on. for ((i = 0; i < ${#passwords[@]}; i++)); do if [[ -z "${passwords[i]}" ]]; then warn "User ${usernames[i]} will not be created as its password is empty or not set. MongoDB cannot create users with blank passwords." fi done fi if [[ -n "$MONGODB_USERNAME" ]] && [[ -z "$MONGODB_PASSWORD" ]]; then warn "User $MONGODB_USERNAME will not be created as its password is empty or not set. MongoDB cannot create users with blank passwords." fi if ! is_boolean_yes "$ALLOW_EMPTY_PASSWORD" && [[ -n "$MONGODB_METRICS_USERNAME" ]] && [[ -z "$MONGODB_METRICS_PASSWORD" ]]; then error_message="The MONGODB_METRICS_PASSWORD environment variable is empty or not set. Set the environment variable ALLOW_EMPTY_PASSWORD=yes to allow the container to be started with blank passwords. This is only recommended for development." print_validation_error "$error_message" fi [[ "$error_code" -eq 0 ]] || exit "$error_code" } ######################## # Copy mounted configuration files # Globals: # MONGODB_* # Arguments: # None # Returns: # None ######################### mongodb_copy_mounted_config() { if ! is_dir_empty "$MONGODB_MOUNTED_CONF_DIR"; then if ! cp -Lr "$MONGODB_MOUNTED_CONF_DIR"/* "$MONGODB_CONF_DIR"; then error "Issue copying mounted configuration files from $MONGODB_MOUNTED_CONF_DIR to $MONGODB_CONF_DIR. Make sure you are not mounting configuration files in $MONGODB_CONF_DIR and $MONGODB_MOUNTED_CONF_DIR at the same time" exit 1 fi fi } ######################## # Determine the hostname by which to contact the locally running mongo daemon # Globals: # MONGODB_* # Arguments: # None # Returns: # The value of get_machine_ip, $MONGODB_ADVERTISED_HOSTNAME or the current host address ######################## get_mongo_hostname() { if is_boolean_yes "$MONGODB_ADVERTISE_IP"; then get_machine_ip elif [[ -n "$MONGODB_ADVERTISED_HOSTNAME" ]]; then echo "$MONGODB_ADVERTISED_HOSTNAME" else hostname fi } ######################## # Determine the port on which to contact the locally running mongo daemon # Globals: # MONGODB_* # Arguments: # None # Returns: # The value of $MONGODB_ADVERTISED_PORT_NUMBER or $MONGODB_PORT_NUMBER ######################## get_mongo_port() { if [[ -n "$MONGODB_ADVERTISED_PORT_NUMBER" ]]; then echo "$MONGODB_ADVERTISED_PORT_NUMBER" else echo "$MONGODB_PORT_NUMBER" fi } ######################## # Drop local Database # Globals: # MONGODB_* # Arguments: # None # Returns: # None ######################### mongodb_drop_local_database() { info "Dropping local database to reset replica set setup..." local command=("mongodb_execute") if [[ -n "$MONGODB_USERNAME" ]] || [[ -n "$MONGODB_EXTRA_USERNAMES" ]]; then local usernames passwords databases mongodb_auth command=("${command[@]}" "${usernames[0]}" "${passwords[0]}") fi "${command[@]}" <"$conf_file_path" } ######################## # Change common logging settings # Globals: # MONGODB_* # Arguments: # None # Returns: # None ######################### mongodb_set_log_conf() { local -r conf_file_path="${1:-$MONGODB_CONF_FILE}" local -r conf_file_name="${conf_file_path#"$MONGODB_CONF_DIR"}" if ! mongodb_is_file_external "$conf_file_name"; then if [[ -n "$MONGODB_DISABLE_SYSTEM_LOG" ]]; then mongodb_config_apply_regex "quiet:.*" "quiet: $({ is_boolean_yes "$MONGODB_DISABLE_SYSTEM_LOG" && echo 'true'; } || echo 'false')" "$conf_file_path" fi if [[ -n "$MONGODB_SYSTEM_LOG_VERBOSITY" ]]; then mongodb_config_apply_regex "verbosity:.*" "verbosity: $MONGODB_SYSTEM_LOG_VERBOSITY" "$conf_file_path" fi else debug "$conf_file_name mounted. Skipping setting log settings" fi } ######################## # Change journaling setting # Globals: # MONGODB_* # Arguments: # None # Returns: # None ######################### mongodb_set_journal_conf() { local -r conf_file_path="${1:-$MONGODB_CONF_FILE}" local -r conf_file_name="${conf_file_path#"$MONGODB_CONF_DIR"}" local mongodb_conf if ! mongodb_is_file_external "$conf_file_name"; then # Disable journal.enabled since it is not supported from 7.0 on if [[ "$(mongodb_get_version)" =~ ^7\..\. ]]; then mongodb_conf="$(sed '/journal:/,/enabled: .*/d' "$conf_file_path")" echo "$mongodb_conf" >"$conf_file_path" else if [[ -n "$MONGODB_ENABLE_JOURNAL" ]]; then mongodb_conf="$(sed -E "/^ *journal:/,/^ *[^:]*:/s/enabled:.*/enabled: $({ is_boolean_yes "$MONGODB_ENABLE_JOURNAL" && echo 'true'; } || echo 'false')/" "$conf_file_path")" echo "$mongodb_conf" >"$conf_file_path" fi fi else debug "$conf_file_name mounted. Skipping setting log settings" fi } ######################## # Change common storage settings # Globals: # MONGODB_* # Arguments: # None # Returns: # None ######################### mongodb_set_storage_conf() { local -r conf_file_path="${1:-$MONGODB_CONF_FILE}" local -r conf_file_name="${conf_file_path#"$MONGODB_CONF_DIR"}" if ! mongodb_is_file_external "$conf_file_name"; then if [[ -n "$MONGODB_ENABLE_DIRECTORY_PER_DB" ]]; then mongodb_config_apply_regex "directoryPerDB:.*" "directoryPerDB: $({ is_boolean_yes "$MONGODB_ENABLE_DIRECTORY_PER_DB" && echo 'true'; } || echo 'false')" "$conf_file_path" fi else debug "$conf_file_name mounted. Skipping setting storage settings" fi } ######################## # Change common network settings # Globals: # MONGODB_* # Arguments: # None # Returns: # None ######################### mongodb_set_net_conf() { local -r conf_file_path="${1:-$MONGODB_CONF_FILE}" local -r conf_file_name="${conf_file_path#"$MONGODB_CONF_DIR"}" if ! mongodb_is_file_external "$conf_file_name"; then if [[ -n "$MONGODB_PORT_NUMBER" ]]; then mongodb_config_apply_regex "port:.*" "port: $MONGODB_PORT_NUMBER" "$conf_file_path" fi if [[ -n "$MONGODB_ENABLE_IPV6" ]]; then mongodb_config_apply_regex "ipv6:.*" "ipv6: $({ is_boolean_yes "$MONGODB_ENABLE_IPV6" && echo 'true'; } || echo 'false')" "$conf_file_path" fi else debug "$conf_file_name mounted. Skipping setting port and IPv6 settings" fi } ######################## # Change bind ip address to 0.0.0.0 # Globals: # MONGODB_* # Arguments: # None # Returns: # None ######################### mongodb_set_listen_all_conf() { local -r conf_file_path="${1:-$MONGODB_CONF_FILE}" local -r conf_file_name="${conf_file_path#"$MONGODB_CONF_DIR"}" if ! mongodb_is_file_external "$conf_file_name"; then mongodb_config_apply_regex "#?bindIp:.*" "#bindIp:" "$conf_file_path" mongodb_config_apply_regex "#?bindIpAll:.*" "bindIpAll: true" "$conf_file_path" else debug "$conf_file_name mounted. Skipping IP binding to all addresses" fi } ######################## # Disable javascript # Globals: # MONGODB_* # Arguments: # None # Returns: # None ######################### mongodb_disable_javascript_conf() { local -r conf_file_path="${1:-$MONGODB_CONF_FILE}" local -r conf_file_name="${conf_file_path#"$MONGODB_CONF_DIR"}" if ! mongodb_is_file_external "$conf_file_name"; then mongodb_config_apply_regex "#?security:" "security:\n javascriptEnabled: false" "$conf_file_path" else debug "$conf_file_name mounted. Skipping disabling javascript" fi } ######################## # Enable Auth # Globals: # MONGODB_* # Arguments: # None # Return # None ######################### mongodb_set_auth_conf() { local -r conf_file_path="${1:-$MONGODB_CONF_FILE}" local -r conf_file_name="${conf_file_path#"$MONGODB_CONF_DIR"}" local authorization if ! mongodb_is_file_external "$conf_file_name"; then if [[ -n "$MONGODB_ROOT_PASSWORD" ]] || [[ -n "$MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD" ]] || [[ -n "$MONGODB_PASSWORD" ]]; then authorization="$(yq eval .security.authorization "$MONGODB_CONF_FILE")" if [[ "$authorization" = "disabled" ]]; then info "Enabling authentication..." # TODO: replace 'sed' calls with 'yq' once 'yq write' does not remove comments mongodb_config_apply_regex "#?authorization:.*" "authorization: enabled" "$conf_file_path" mongodb_config_apply_regex "#?enableLocalhostAuthBypass:.*" "enableLocalhostAuthBypass: false" "$conf_file_path" fi fi else debug "$conf_file_name mounted. Skipping authorization enabling" fi } ######################## # Enable ReplicaSetMode # Globals: # MONGODB_* # Arguments: # None # Returns: # None ######################### mongodb_set_replicasetmode_conf() { local -r conf_file_path="${1:-$MONGODB_CONF_FILE}" local -r conf_file_name="${conf_file_path#"$MONGODB_CONF_DIR"}" if ! mongodb_is_file_external "$conf_file_name"; then mongodb_config_apply_regex "#?replication:.*" "replication:" "$conf_file_path" mongodb_config_apply_regex "#?replSetName:" "replSetName:" "$conf_file_path" mongodb_config_apply_regex "#?enableMajorityReadConcern:.*" "enableMajorityReadConcern:" "$conf_file_path" if [[ -n "$MONGODB_REPLICA_SET_NAME" ]]; then mongodb_config_apply_regex "replSetName:.*" "replSetName: $MONGODB_REPLICA_SET_NAME" "$conf_file_path" fi if [[ -n "$MONGODB_ENABLE_MAJORITY_READ" ]]; then mongodb_config_apply_regex "enableMajorityReadConcern:.*" "enableMajorityReadConcern: $({ (is_boolean_yes "$MONGODB_ENABLE_MAJORITY_READ" || [[ "$(mongodb_get_version)" =~ ^5\..\. ]]) && echo 'true'; } || echo 'false')" "$conf_file_path" fi else debug "$conf_file_name mounted. Skipping replicaset mode enabling" fi } ######################## # Create a MongoDB user and provide read/write permissions on a database # Globals: # MONGODB_ROOT_PASSWORD # Arguments: # $1 - Name of user # $2 - Password for user # $3 - Name of database (empty for default database) # Returns: # None ######################### mongodb_create_user() { local -r user="${1:?user is required}" local -r password="${2:-}" local -r database="${3:-}" local query if [[ -z "$password" ]]; then warn "Cannot create user '$user', no password provided" return 0 fi # Build proper query (default database or specific one) query="db.getSiblingDB('$database').createUser({ user: '$user', pwd: '$password', roles: [{role: 'readWrite', db: '$database'}] })" [[ -z "$database" ]] && query="db.getSiblingDB(db.stats().db).createUser({ user: '$user', pwd: '$password', roles: [{role: 'readWrite', db: db.getSiblingDB(db.stats().db).stats().db }] })" # Create user, discarding mongo CLI output for clean logs info "Creating user '$user'..." mongodb_execute "$MONGODB_ROOT_USER" "$MONGODB_ROOT_PASSWORD" "" "127.0.0.1" <<<"$query" } ######################## # Create the appropriate users # Globals: # MONGODB_* # Arguments: # None # Returns: # None ######################### mongodb_create_users() { info "Creating users..." if [[ -n "$MONGODB_ROOT_PASSWORD" ]] && ! [[ "$MONGODB_REPLICA_SET_MODE" =~ ^(secondary|arbiter|hidden) ]]; then info "Creating $MONGODB_ROOT_USER user..." mongodb_execute "" "" "" "127.0.0.1" <"$MONGODB_KEY_FILE" chmod 600 "$MONGODB_KEY_FILE" if am_i_root; then configure_permissions "$MONGODB_KEY_FILE" "$MONGODB_DAEMON_USER" "$MONGODB_DAEMON_GROUP" "" "600" else chmod 600 "$MONGODB_KEY_FILE" fi else debug "keyfile mounted. Skipping keyfile generation" fi } ######################## # Get if primary node is initialized # Globals: # MONGODB_* # Arguments: # $1 - node # $2 - port # Returns: # None ######################### mongodb_is_primary_node_initiated() { local node="${1:?node is required}" local port="${2:?port is required}" local result result=$( mongodb_execute_print_output "$MONGODB_ROOT_USER" "$MONGODB_ROOT_PASSWORD" "admin" "127.0.0.1" "$MONGODB_PORT_NUMBER" < m.name === '$node:$port' && m.stateStr === 'SECONDARY').length === 1 EOF ) debug "$result" grep -q "true" <<<"$result" } ######################## # Grant voting rights to secondary node # Globals: # MONGODB_* # Arguments: # $1 - node # $2 - port # Returns: # Boolean ######################### mongodb_configure_secondary_node_voting() { local -r node="${1:?node is required}" local -r port="${2:?port is required}" debug "Granting voting rights to the node" local reconfig_cmd="rs.reconfigForPSASet(member, cfg)" [[ "$(mongodb_get_version)" =~ ^4\.(0|2)\. ]] && reconfig_cmd="rs.reconfig(cfg)" result=$( mongodb_execute_print_output "$MONGODB_INITIAL_PRIMARY_ROOT_USER" "$MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD" "admin" "$MONGODB_INITIAL_PRIMARY_HOST" "$MONGODB_INITIAL_PRIMARY_PORT_NUMBER" < m.host === '$node:$port') cfg.members[member].priority = 1 cfg.members[member].votes = 1 $reconfig_cmd EOF ) debug "$result" grep -q "ok: 1" <<<"$result" } ######################## # Get if hidden node is pending # Globals: # MONGODB_* # Arguments: # $1 - node # $2 - port # Returns: # Boolean ######################### mongodb_is_hidden_node_pending() { local node="${1:?node is required}" local port="${2:?port is required}" local result mongodb_set_dwc debug "Adding hidden node ${node}:${port}" result=$( mongodb_execute_print_output "$MONGODB_INITIAL_PRIMARY_ROOT_USER" "$MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD" "admin" "$MONGODB_INITIAL_PRIMARY_HOST" "$MONGODB_INITIAL_PRIMARY_PORT_NUMBER" <