diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a221025 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +FROM registry.fedoraproject.org/fedora:26 + +ENV NAME=mongodb ARCH=x86_64 +LABEL MAINTAINER "Matus Kocka" +LABEL summary="MongoDB is a scalable, high-performance, open source NoSQL database." + +EXPOSE 27017 + +RUN INSTALL_PKGS="bind-utils gettext iproute rsync tar findutils python3 mongodb mongodb-server mongo-tools" && \ + dnf install -y --setopt=tsflags=nodocs $INSTALL_PKGS && \ + rpm -V $INSTALL_PKGS && \ + dnf clean all + +ADD files / + +ENTRYPOINT ["container-entrypoint"] +CMD ["run-mongod"] + +# Container setup from scl +RUN : > /etc/mongod.conf && \ + mkdir -p ${HOME}/data && \ + # Set owner 'mongodb:0' and 'g+rw(x)' permission - to avoid problems running container with arbitrary UID + /usr/libexec/fix-permissions /etc/mongod.conf ${CONTAINER_SCRIPTS_PATH}/mongodb.conf.template \ + ${HOME} + +VOLUME ["/var/lib/mongodb/data"] + +USER 184 + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6aa9fdd --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +IMAGE_NAME = mongodb + +MODULEMDURL=file://mongodb.yaml + +default: run + +build: + docker build --tag=$(IMAGE_NAME) . + +run: build + docker run -d $(IMAGE_NAME) + +test: build + cd tests; MODULE=docker MODULEMD=$(MODULEMDURL) URL="docker=$(IMAGE_NAME)" make all + cd tests; MODULE=rpm MODULEMD=$(MODULEMDURL) URL="docker=$(IMAGE_NAME)" make all diff --git a/files/usr/bin/cgroup-limits b/files/usr/bin/cgroup-limits new file mode 100755 index 0000000..b9d4edc --- /dev/null +++ b/files/usr/bin/cgroup-limits @@ -0,0 +1,92 @@ +#!/usr/bin/python + +""" +Script for parsing cgroup information + +This script will read some limits from the cgroup system and parse +them, printing out "VARIABLE=VALUE" on each line for every limit that is +successfully read. Output of this script can be directly fed into +bash's export command. Recommended usage from a bash script: + + set -o errexit + export_vars=$(cgroup-limits) ; export $export_vars + +Variables currently supported: + MAX_MEMORY_LIMIT_IN_BYTES + Maximum possible limit MEMORY_LIMIT_IN_BYTES can have. This is + currently constant value of 9223372036854775807. + MEMORY_LIMIT_IN_BYTES + Maximum amount of user memory in bytes. If this value is set + to the same value as MAX_MEMORY_LIMIT_IN_BYTES, it means that + there is no limit set. The value is taken from + /sys/fs/cgroup/memory/memory.limit_in_bytes + NUMBER_OF_CORES + Number of detected CPU cores that can be used. This value is + calculated from /sys/fs/cgroup/cpuset/cpuset.cpus + NO_MEMORY_LIMIT + Set to "true" if MEMORY_LIMIT_IN_BYTES is so high that the caller + can act as if no memory limit was set. Undefined otherwise. +""" + +from __future__ import print_function +import sys + + +def _read_file(path): + try: + with open(path, 'r') as f: + return f.read().strip() + except IOError: + return None + + +def get_memory_limit(): + """ + Read memory limit, in bytes. + """ + + limit = _read_file('/sys/fs/cgroup/memory/memory.limit_in_bytes') + if limit is None or not limit.isdigit(): + print("Warning: Can't detect memory limit from cgroups", + file=sys.stderr) + return None + return int(limit) + + +def get_number_of_cores(): + """ + Read number of CPU cores. + """ + + core_count = 0 + + line = _read_file('/sys/fs/cgroup/cpuset/cpuset.cpus') + if line is None: + print("Warning: Can't detect number of CPU cores from cgroups", + file=sys.stderr) + return None + + for group in line.split(','): + core_ids = list(map(int, group.split('-'))) + if len(core_ids) == 2: + core_count += core_ids[1] - core_ids[0] + 1 + else: + core_count += 1 + + return core_count + + +if __name__ == "__main__": + env_vars = { + "MAX_MEMORY_LIMIT_IN_BYTES": 9223372036854775807, + "MEMORY_LIMIT_IN_BYTES": get_memory_limit(), + "NUMBER_OF_CORES": get_number_of_cores() + } + + env_vars = {k: v for k, v in env_vars.items() if v is not None} + + if env_vars.get("MEMORY_LIMIT_IN_BYTES", 0) >= 92233720368547: + env_vars["NO_MEMORY_LIMIT"] = "true" + + for key, value in env_vars.items(): + print("{0}={1}".format(key, value)) diff --git a/files/usr/bin/container-entrypoint b/files/usr/bin/container-entrypoint new file mode 100755 index 0000000..9d8ad4d --- /dev/null +++ b/files/usr/bin/container-entrypoint @@ -0,0 +1,2 @@ +#!/bin/bash +exec "$@" diff --git a/files/usr/bin/run-mongod b/files/usr/bin/run-mongod new file mode 100755 index 0000000..e273e48 --- /dev/null +++ b/files/usr/bin/run-mongod @@ -0,0 +1,57 @@ +#!/bin/bash + +set -o errexit +set -o nounset +set -o pipefail + +source ${CONTAINER_SCRIPTS_PATH}/common.sh + +function cleanup() { + echo "=> Shutting down MongoDB server ..." + pkill -INT mongod || : + wait_for_mongo_down + exit 0 +} + +trap 'cleanup' SIGINT SIGTERM + +check_env_vars + +setup_wiredtiger_cache ${CONTAINER_SCRIPTS_PATH}/mongodb.conf.template + +# If user provides own config file use it and do not generate new one +if [ ! -s $MONGODB_CONFIG_PATH ]; then + # Generate config file for MongoDB + envsubst < ${CONTAINER_SCRIPTS_PATH}/mongodb.conf.template > $MONGODB_CONFIG_PATH +fi + +mongo_common_args="-f $MONGODB_CONFIG_PATH" + +setup_default_datadir + +# Must bring up MongoDB on localhost only until it has an admin password set. +mongod $mongo_common_args --bind_ip 127.0.0.1 & +wait_for_mongo_up +js_command="db.system.users.count({'user':'admin', 'db':'admin'})" +if [ "$(mongo admin --quiet --eval "$js_command")" == "1" ]; then + echo "=> Admin user is already created. Resetting password ..." + mongo_reset_admin +else + mongo_create_admin +fi +if [[ -v CREATE_USER ]]; then + js_command="db.system.users.count({'user':'${MONGODB_USER}', 'db':'${MONGODB_DATABASE}'})" + if [ "$(mongo admin --quiet --eval "$js_command")" == "1" ]; then + echo "=> MONGODB_USER user is already created. Resetting password ..." + mongo_reset_user + else + mongo_create_user + fi +fi +# Restart the MongoDB daemon to bind on all interfaces +mongod $mongo_common_args --shutdown +wait_for_mongo_down + +# Make sure env variables don't propagate to mongod process. +unset MONGODB_USER MONGODB_PASSWORD MONGODB_DATABASE MONGODB_ADMIN_PASSWORD +exec mongod $mongo_common_args --auth diff --git a/files/usr/bin/run-mongod-pet b/files/usr/bin/run-mongod-pet new file mode 100755 index 0000000..fb803e1 --- /dev/null +++ b/files/usr/bin/run-mongod-pet @@ -0,0 +1,55 @@ +#!/bin/bash +# +# Run mongod in a StatefulSet-based replica set. See +# https://github.com/sclorg/mongodb-container/blob/master/examples/petset/README.md +# for a description of how this is intended to work. +# +# Note: +# - It does not attempt to remove the host from the replica set configuration +# when it is terminating. That is by design, because, in a StatefulSet, when a +# pod/container terminates and is restarted by OpenShift, it will always have +# the same hostname. Removing hosts from the configuration affects replica set +# elections and can impact the replica set stability. + +set -o errexit +set -o nounset +set -o pipefail + +source ${CONTAINER_SCRIPTS_PATH}/common.sh + +function cleanup() { + echo "=> Shutting down MongoDB server ..." + pkill -INT mongod || : + wait_for_mongo_down + exit 0 +} + +trap 'cleanup' SIGINT SIGTERM + +REPLICATION=1 check_env_vars + +setup_wiredtiger_cache ${CONTAINER_SCRIPTS_PATH}/mongodb.conf.template + +# If user provides own config file use it and do not generate new one +if [ ! -s "${MONGODB_CONFIG_PATH}" ]; then + # Generate config file for MongoDB + envsubst < "${CONTAINER_SCRIPTS_PATH}/mongodb.conf.template" > "${MONGODB_CONFIG_PATH}" +fi + +mongo_common_args="-f ${MONGODB_CONFIG_PATH}" + +# Attention: setup_keyfile may modify value of mongo_common_args! +setup_keyfile +setup_default_datadir + +${CONTAINER_SCRIPTS_PATH}/init-replset.sh & + +# TODO: capture exit code of `init-petset-replset.sh` and exit with an error if +# the initialization failed, so that the container will be restarted and the +# user can gain more visibility that there is a problem in a way other than just +# inspecting log messages. + +# Make sure env variables don't propagate to mongod process. +unset MONGODB_USER MONGODB_PASSWORD MONGODB_DATABASE MONGODB_ADMIN_PASSWORD +mongod ${mongo_common_args} --replSet "${MONGODB_REPLICA_NAME}" & +wait diff --git a/files/usr/bin/run-mongod-replication b/files/usr/bin/run-mongod-replication new file mode 100755 index 0000000..fb803e1 --- /dev/null +++ b/files/usr/bin/run-mongod-replication @@ -0,0 +1,55 @@ +#!/bin/bash +# +# Run mongod in a StatefulSet-based replica set. See +# https://github.com/sclorg/mongodb-container/blob/master/examples/petset/README.md +# for a description of how this is intended to work. +# +# Note: +# - It does not attempt to remove the host from the replica set configuration +# when it is terminating. That is by design, because, in a StatefulSet, when a +# pod/container terminates and is restarted by OpenShift, it will always have +# the same hostname. Removing hosts from the configuration affects replica set +# elections and can impact the replica set stability. + +set -o errexit +set -o nounset +set -o pipefail + +source ${CONTAINER_SCRIPTS_PATH}/common.sh + +function cleanup() { + echo "=> Shutting down MongoDB server ..." + pkill -INT mongod || : + wait_for_mongo_down + exit 0 +} + +trap 'cleanup' SIGINT SIGTERM + +REPLICATION=1 check_env_vars + +setup_wiredtiger_cache ${CONTAINER_SCRIPTS_PATH}/mongodb.conf.template + +# If user provides own config file use it and do not generate new one +if [ ! -s "${MONGODB_CONFIG_PATH}" ]; then + # Generate config file for MongoDB + envsubst < "${CONTAINER_SCRIPTS_PATH}/mongodb.conf.template" > "${MONGODB_CONFIG_PATH}" +fi + +mongo_common_args="-f ${MONGODB_CONFIG_PATH}" + +# Attention: setup_keyfile may modify value of mongo_common_args! +setup_keyfile +setup_default_datadir + +${CONTAINER_SCRIPTS_PATH}/init-replset.sh & + +# TODO: capture exit code of `init-petset-replset.sh` and exit with an error if +# the initialization failed, so that the container will be restarted and the +# user can gain more visibility that there is a problem in a way other than just +# inspecting log messages. + +# Make sure env variables don't propagate to mongod process. +unset MONGODB_USER MONGODB_PASSWORD MONGODB_DATABASE MONGODB_ADMIN_PASSWORD +mongod ${mongo_common_args} --replSet "${MONGODB_REPLICA_NAME}" & +wait diff --git a/files/usr/libexec/fix-permissions b/files/usr/libexec/fix-permissions new file mode 100755 index 0000000..6ad8d03 --- /dev/null +++ b/files/usr/libexec/fix-permissions @@ -0,0 +1,6 @@ +#!/bin/sh +# Fix permissions on the given directory to allow group read/write of +# regular files and execute of directories. +find $@ -exec chown mongodb:0 {} \; +find $@ -exec chmod g+rw {} \; +find $@ -type d -exec chmod g+x {} + diff --git a/files/usr/share/container-scripts/mongodb/README.md b/files/usr/share/container-scripts/mongodb/README.md new file mode 100644 index 0000000..28b8988 --- /dev/null +++ b/files/usr/share/container-scripts/mongodb/README.md @@ -0,0 +1,90 @@ +MongoDB Docker image +==================== + +This repository contains Dockerfiles for MongoDB images for general usage and OpenShift. +Users can choose between RHEL and CentOS based images. + +Environment variables +--------------------------------- + +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 | +| :------------------------ | ----------------------------------------- | +| `MONGODB_ADMIN_PASSWORD` | Password for the admin user | + +Optionally you can provide settings for user with 'readWrite' role. + +| Variable name | Description | +| :------------------------ | ----------------------------------------- | +| `MONGODB_USER` | User name for MONGODB account to be created | +| `MONGODB_PASSWORD` | Password for the user account | +| `MONGODB_DATABASE` | Database name | + + +The following environment variables influence the MongoDB configuration file. They are all optional. + +| Variable name | Description | Default +| :-------------------- | ------------------------------------------------------------------------- | ---------------- +| `MONGODB_QUIET` | Runs MongoDB in a quiet mode that attempts to limit the amount of output. | true + + +You can also set the following mount points by passing the `-v /host:/container` flag to Docker. + +| Volume mount point | Description | +| :-------------------------- | ---------------------- | +| `/var/lib/mongodb/data` | MongoDB 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 `centos/mongodb-32-centos7` image. +If you want to set only the mandatory environment variables and store the database +in the `/home/user/database` directory on the host filesystem, execute the following command: + +``` +$ docker run -d -e MONGODB_USER= -e MONGODB_PASSWORD= -e MONGODB_DATABASE= -e MONGODB_ADMIN_PASSWORD= -v /home/user/database:/var/lib/mongodb/data centos/mongodb-32-centos7 +``` + +If you are initializing the database and it's the first time you are using the +specified shared volume, the database will be created with two users: `admin` and `MONGODB_USER`. After that the MongoDB daemon +will be started. If you are re-attaching the volume to another container, the +creation of the database user and admin user will be skipped and only the +MongoDB daemon will be started. + +Custom configuration file +--------------------------------- + +It is allowed to use custom configuration file for mongod server. Providing a custom configuration file supercedes the individual configuration environment variable values. + +To use custom configuration file in container it has to be mounted into `/etc/mongod.conf`. For example to use configuration file stored in `/home/user` directory use this option for `docker run` command: `-v /home/user/mongod.conf:/etc/mongod.conf:Z`. + +**Notice: Custom config file does not affect name of replica set. It has to be set in `MONGODB_REPLICA_NAME` environment variable.** + +MongoDB admin user +--------------------------------- + +The admin user name is set to `admin` and you have to to specify the password by +setting the `MONGODB_ADMIN_PASSWORD` environment variable. This process is done +upon database initialization. + + +Changing passwords +------------------ + +Since passwords are part of the image configuration, the only supported method +to change passwords for the database user (`MONGODB_USER`) and admin user is by +changing the environment variables `MONGODB_PASSWORD` and +`MONGODB_ADMIN_PASSWORD`, respectively. + +Changing database passwords directly in MongoDB 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. + diff --git a/files/usr/share/container-scripts/mongodb/common.sh b/files/usr/share/container-scripts/mongodb/common.sh new file mode 100644 index 0000000..c6dcfc2 --- /dev/null +++ b/files/usr/share/container-scripts/mongodb/common.sh @@ -0,0 +1,260 @@ +#!/bin/bash + +set -o errexit +set -o nounset +set -o pipefail + +# Data directory where MongoDB database files live. The data subdirectory is here +# because mongodb.conf lives in /var/lib/mongodb/ and we don't want a volume to +# override it. +export MONGODB_DATADIR=/var/lib/mongodb/data +export CONTAINER_PORT=27017 +# Configuration settings. +export MONGODB_QUIET=${MONGODB_QUIET:-true} + +MONGODB_CONFIG_PATH=/etc/mongod.conf +MONGODB_KEYFILE_PATH="${HOME}/keyfile" + +# Constants used for waiting +readonly MAX_ATTEMPTS=60 +readonly SLEEP_TIME=1 + +# wait_for_mongo_up waits until the mongo server accepts incomming connections +function wait_for_mongo_up() { + _wait_for_mongo 1 "$@" +} + +# wait_for_mongo_down waits until the mongo server is down +function wait_for_mongo_down() { + _wait_for_mongo 0 "$@" +} + +# wait_for_mongo waits until the mongo server is up/down +# $1 - 0 or 1 - to specify for what to wait (0 - down, 1 - up) +# $2 - host where to connect (localhost by default) +function _wait_for_mongo() { + local operation=${1:-1} + local message="up" + if [[ ${operation} -eq 0 ]]; then + message="down" + fi + + local mongo_cmd="mongo admin --host ${2:-localhost} " + + local i + for i in $(seq $MAX_ATTEMPTS); do + echo "=> ${2:-} Waiting for MongoDB daemon ${message}" + if ([[ ${operation} -eq 1 ]] && ${mongo_cmd} --eval "quit()" &>/dev/null) || ([[ ${operation} -eq 0 ]] && ! ${mongo_cmd} --eval "quit()" &>/dev/null); then + echo "=> MongoDB daemon is ${message}" + return 0 + fi + sleep ${SLEEP_TIME} + done + echo "=> Giving up: MongoDB daemon is not ${message}!" + return 1 +} + +# endpoints returns list of IP addresses with other instances of MongoDB +# To get list of endpoints, you need to have headless Service named 'mongodb'. +# NOTE: This won't work with standalone Docker container. +function endpoints() { + service_name=${MONGODB_SERVICE_NAME:-mongodb} + dig ${service_name} A +search +short 2>/dev/null +} + +# replset_addr return the address of the current replSet +function replset_addr() { + local current_endpoints + current_endpoints="$(endpoints)" + if [ -z "${current_endpoints}" ]; then + info "Cannot get address of replica set: no nodes are listed in service!" + info "CAUSE: DNS lookup for '${MONGODB_SERVICE_NAME:-mongodb}' returned no results." + return 1 + fi + echo "${MONGODB_REPLICA_NAME}/${current_endpoints//[[:space:]]/,}" +} + +# mongo_create_admin creates the MongoDB admin user with password: MONGODB_ADMIN_PASSWORD +# $1 - login parameters for mongo (optional) +# $2 - host where to connect (localhost by default) +function mongo_create_admin() { + if [[ -z "${MONGODB_ADMIN_PASSWORD:-}" ]]; then + echo >&2 "=> MONGODB_ADMIN_PASSWORD is not set. Authentication can not be set up." + exit 1 + fi + + # Set admin password + local js_command="db.createUser({user: 'admin', pwd: '${MONGODB_ADMIN_PASSWORD}', roles: ['dbAdminAnyDatabase', 'userAdminAnyDatabase' , 'readWriteAnyDatabase','clusterAdmin' ]});" + if ! mongo admin ${1:-} --host ${2:-"localhost"} --eval "${js_command}"; then + echo >&2 "=> Failed to create MongoDB admin user." + exit 1 + fi +} + +# mongo_create_user creates the MongoDB database user: MONGODB_USER, +# with password: MONGDOB_PASSWORD, inside database: MONGODB_DATABASE +# $1 - login parameters for mongo (optional) +# $2 - host where to connect (localhost by default) +function mongo_create_user() { + # Ensure input variables exists + if [[ -z "${MONGODB_USER:-}" ]]; then + echo >&2 "=> MONGODB_USER is not set. Failed to create MongoDB user" + exit 1 + fi + if [[ -z "${MONGODB_PASSWORD:-}" ]]; then + echo "=> MONGODB_PASSWORD is not set. Failed to create MongoDB user: ${MONGODB_USER}" + exit >&2 1 + fi + if [[ -z "${MONGODB_DATABASE:-}" ]]; then + echo >&2 "=> MONGODB_DATABASE is not set. Failed to create MongoDB user: ${MONGODB_USER}" + exit 1 + fi + + # Create database user + local js_command="db.getSiblingDB('${MONGODB_DATABASE}').createUser({user: '${MONGODB_USER}', pwd: '${MONGODB_PASSWORD}', roles: [ 'readWrite' ]});" + if ! mongo admin ${1:-} --host ${2:-"localhost"} --eval "${js_command}"; then + echo >&2 "=> Failed to create MongoDB user: ${MONGODB_USER}" + exit 1 + fi +} + +# mongo_reset_user sets the MongoDB MONGODB_USER's password to match MONGODB_PASSWORD +function mongo_reset_user() { + if [[ -n "${MONGODB_USER:-}" && -n "${MONGODB_PASSWORD:-}" && -n "${MONGODB_DATABASE:-}" ]]; then + local js_command="db.changeUserPassword('${MONGODB_USER}', '${MONGODB_PASSWORD}')" + if ! mongo ${MONGODB_DATABASE} --eval "${js_command}"; then + echo >&2 "=> Failed to reset password of MongoDB user: ${MONGODB_USER}" + exit 1 + fi + fi +} + +# mongo_reset_admin sets the MongoDB admin password to match MONGODB_ADMIN_PASSWORD +function mongo_reset_admin() { + if [[ -n "${MONGODB_ADMIN_PASSWORD:-}" ]]; then + local js_command="db.changeUserPassword('admin', '${MONGODB_ADMIN_PASSWORD}')" + if ! mongo admin --eval "${js_command}"; then + echo >&2 "=> Failed to reset password of MongoDB user: ${MONGODB_USER}" + exit 1 + fi + fi +} + +# setup_keyfile fixes the bug in mounting the Kubernetes 'Secret' volume that +# mounts the secret files with 'too open' permissions. +# add --keyFile argument to mongo_common_args +function setup_keyfile() { + # If user specify keyFile in config file do not use generated keyFile + if grep -q "^\s*keyFile" ${MONGODB_CONFIG_PATH}; then + exit 0 + fi + if [ -z "${MONGODB_KEYFILE_VALUE-}" ]; then + echo >&2 "ERROR: You have to provide the 'keyfile' value in MONGODB_KEYFILE_VALUE" + exit 1 + fi + local keyfile_dir + keyfile_dir="$(dirname "$MONGODB_KEYFILE_PATH")" + if [ ! -w "$keyfile_dir" ]; then + echo >&2 "ERROR: Couldn't create ${MONGODB_KEYFILE_PATH}" + echo >&2 "CAUSE: current user doesn't have permissions for writing to ${keyfile_dir} directory" + echo >&2 "DETAILS: current user id = $(id -u), user groups: $(id -G)" + echo >&2 "DETAILS: directory permissions: $(stat -c '%A owned by %u:%g' "${keyfile_dir}")" + exit 1 + fi + echo ${MONGODB_KEYFILE_VALUE} > ${MONGODB_KEYFILE_PATH} + chmod 0600 ${MONGODB_KEYFILE_PATH} + mongo_common_args+=" --keyFile ${MONGODB_KEYFILE_PATH}" +} + +# setup_default_datadir checks permissions of mounded directory into default +# data directory MONGODB_DATADIR +function setup_default_datadir() { + if [ ! -w "$MONGODB_DATADIR" ]; then + echo >&2 "ERROR: Couldn't write into ${MONGODB_DATADIR}" + echo >&2 "CAUSE: current user doesn't have permissions for writing to ${MONGODB_DATADIR} directory" + echo >&2 "DETAILS: current user id = $(id -u), user groups: $(id -G)" + echo >&2 "DETAILS: directory permissions: $(stat -c '%A owned by %u:%g, SELinux: %C' "${MONGODB_DATADIR}")" + exit 1 + fi +} + +# setup_wiredtiger_cache checks amount of available RAM (it has to use cgroups in container) +# and if there are any memory restrictions set storage.wiredTiger.engineConfig.cacheSizeGB +# in MONGODB_CONFIG_PATH to upstream default size +# it is intended to update mongodb.conf.template, with custom config file it might create conflict +function setup_wiredtiger_cache() { + local config_file + config_file=${1:-$MONGODB_CONFIG_PATH} + + declare $(cgroup-limits) + if [[ ! -v MEMORY_LIMIT_IN_BYTES || "${NO_MEMORY_LIMIT:-}" == "true" ]]; then + return 0; + fi + + cache_size=$(python -c "min=1; limit=int(($MEMORY_LIMIT_IN_BYTES / pow(2,30) - 1) * 0.6); print( min if limit < min else limit)") + echo "storage.wiredTiger.engineConfig.cacheSizeGB: ${cache_size}" >> ${config_file} + + info "wiredTiger cacheSizeGB set to ${cache_size}" +} + +# check_env_vars checks environmental variables +# if variables to create non-admin user are provided, sets CREATE_USER=1 +# if REPLICATION variable is set, checks also replication variables +function check_env_vars() { + local readonly database_regex='^[^/\. "$]*$' + + [[ -v MONGODB_ADMIN_PASSWORD ]] || usage "MONGODB_ADMIN_PASSWORD has to be set." + + if [[ -v MONGODB_USER || -v MONGODB_PASSWORD || -v MONGODB_DATABASE ]]; then + [[ -v MONGODB_USER && -v MONGODB_PASSWORD && -v MONGODB_DATABASE ]] || usage "You have to set all or none of variables: MONGODB_USER, MONGODB_PASSWORD, MONGODB_DATABASE" + + [[ "${MONGODB_DATABASE}" =~ $database_regex ]] || usage "Database name must match regex: $database_regex" + [ ${#MONGODB_DATABASE} -le 63 ] || usage "Database name too long (maximum 63 characters)" + + export CREATE_USER=1 + fi + + if [[ -v REPLICATION ]]; then + [[ -v MONGODB_KEYFILE_VALUE && -v MONGODB_REPLICA_NAME ]] || usage "MONGODB_KEYFILE_VALUE and MONGODB_REPLICA_NAME have to be set" + fi +} + +# usage prints info about required enviromental variables +# if $1 is passed, prints error message containing $1 +# if REPLICATION variable is set, prints also info about replication variables +function usage() { + if [ $# == 1 ]; then + echo >&2 "error: $1" + fi + + echo " +You must specify the following environment variables: + MONGODB_ADMIN_PASSWORD +Optionally you can provide settings for a user with 'readWrite' role: +(Note you MUST specify all three of these settings) + MONGODB_USER + MONGODB_PASSWORD + MONGODB_DATABASE +Optional settings: + MONGODB_QUIET (default: true)" + + if [[ -v REPLICATION ]]; then + echo " +For replication you must also specify the following environment variables: + MONGODB_KEYFILE_VALUE + MONGODB_REPLICA_NAME +Optional settings: + MONGODB_SERVICE_NAME (default: mongodb) +" + fi + echo " +For more information see /usr/share/container-scripts/mongodb/README.md +within the container or visit https://github.com/sclorgk/mongodb-container/." + + exit 1 +} + +# info prints a message prefixed by date and time. +function info() { + printf "=> [%s] %s\n" "$(date +'%a %b %d %T')" "$*" +} diff --git a/files/usr/share/container-scripts/mongodb/init-replset.sh b/files/usr/share/container-scripts/mongodb/init-replset.sh new file mode 100755 index 0000000..dd7f2de --- /dev/null +++ b/files/usr/share/container-scripts/mongodb/init-replset.sh @@ -0,0 +1,86 @@ +#!/bin/bash + +set -o errexit +set -o nounset +set -o pipefail + +source "${CONTAINER_SCRIPTS_PATH}/common.sh" + +# This is a full hostname that will be added to replica set +# (for example, "replica-2.mongodb.myproject.svc.cluster.local") +readonly MEMBER_HOST="$(hostname -f)" + +# Initializes the replica set configuration. +# +# Arguments: +# - $1: host address[:port] +# +# Uses the following global variables: +# - MONGODB_REPLICA_NAME +# - MONGODB_ADMIN_PASSWORD +function initiate() { + local host="$1" + + local config="{_id: '${MONGODB_REPLICA_NAME}', members: [{_id: 0, host: '${host}'}]}" + + info "Initiating MongoDB replica using: ${config}" + mongo --eval "quit(rs.initiate(${config}).ok ? 0 : 1)" --quiet + + info "Waiting for PRIMARY status ..." + mongo --eval "while (!rs.isMaster().ismaster) { sleep(100); }" --quiet + + info "Creating MongoDB users ..." + mongo_create_admin + [[ -v CREATE_USER ]] && mongo_create_user "-u admin -p ${MONGODB_ADMIN_PASSWORD}" + + info "Successfully initialized replica set" +} + +# Adds a host to the replica set configuration. +# +# Arguments: +# - $1: host address[:port] +# +# Global variables: +# - MONGODB_REPLICA_NAME +# - MONGODB_ADMIN_PASSWORD +function add_member() { + local host="$1" + info "Adding ${host} to replica set ..." + + if ! mongo admin -u admin -p "${MONGODB_ADMIN_PASSWORD}" --host "$(replset_addr)" --eval "while (!rs.add('${host}').ok) { sleep(100); }" --quiet; then + info "ERROR: couldn't add host to replica set!" + return 1 + fi + + info "Waiting for PRIMARY/SECONDARY status ..." + mongo --eval "while (!rs.isMaster().ismaster && !rs.isMaster().secondary) { sleep(100); }" --quiet + + info "Successfully joined replica set" +} + +info "Waiting for local MongoDB to accept connections ..." +wait_for_mongo_up &>/dev/null + +if [[ $(mongo --eval 'db.isMaster().setName' --quiet) == "${MONGODB_REPLICA_NAME}" ]]; then + info "Replica set '${MONGODB_REPLICA_NAME}' already exists, skipping initialization" + >/tmp/initialized + exit 0 +fi + +# StatefulSet pods are named with a predictable name, following the pattern: +# $(statefulset name)-$(zero-based index) +# MEMBER_ID is computed by removing the prefix matching "*-", i.e.: +# "mongodb-0" -> "0" +# "mongodb-1" -> "1" +# "mongodb-2" -> "2" +readonly MEMBER_ID="${HOSTNAME##*-}" + +# Initialize replica set only if we're the first member +if [ "${MEMBER_ID}" = '0' ]; then + initiate "${MEMBER_HOST}" +else + add_member "${MEMBER_HOST}" +fi + +>/tmp/initialized diff --git a/files/usr/share/container-scripts/mongodb/mongodb.conf.template b/files/usr/share/container-scripts/mongodb/mongodb.conf.template new file mode 100644 index 0000000..e33ea21 --- /dev/null +++ b/files/usr/share/container-scripts/mongodb/mongodb.conf.template @@ -0,0 +1,28 @@ +## +## For list of options visit: +## https://docs.mongodb.org/manual/reference/configuration-options/ +## + +# systemLog Options - How to do logging +systemLog: + # Runs the mongod in a quiet mode that attempts to limit the amount of output + quiet: ${MONGODB_QUIET} + + +# net Options - Network interfaces settings +net: + # Specify port number (27017 by default) + port: ${CONTAINER_PORT} + + +# storage Options - How and Where to store data +storage: + # Directory for datafiles (defaults to /data/db/) + dbPath: ${MONGODB_DATADIR} + + +# replication Options - Configures replication +replication: + # Specifies a maximum size in megabytes for the replication operation log (i.e. the oplog, + # 5% of disk space by default) + oplogSizeMB: 64 diff --git a/files/usr/share/container-scripts/mongodb/scl_enable b/files/usr/share/container-scripts/mongodb/scl_enable new file mode 100644 index 0000000..5a25432 --- /dev/null +++ b/files/usr/share/container-scripts/mongodb/scl_enable @@ -0,0 +1,3 @@ +# This will make scl collection binaries work out of box. +unset BASH_ENV PROMPT_COMMAND ENV +source scl_source enable ${ENABLED_COLLECTIONS} diff --git a/files/usr/share/container-scripts/mongodb/test-functions.sh b/files/usr/share/container-scripts/mongodb/test-functions.sh new file mode 100644 index 0000000..50ac0a0 --- /dev/null +++ b/files/usr/share/container-scripts/mongodb/test-functions.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +set -o errexit +set -o nounset +set -o pipefail + +# insert_and_wait_for_replication insert data in host and wait all replset members +# applied recent oplog entry +function insert_and_wait_for_replication() { + local host + host=$1 + local data + data=$2 + + # Storing document into replset and wait replication to finish + local script + script="db.getSiblingDB('test_db').data.insert(${data}); + for (var i = 0; i < 60; i++) { + var status=rs.status(); + var optime=status.members[0].optime; + var ok=true; + for(var j=1; j < status.members.length; j++) { + if(tojson(optime) != tojson(status.members[j].optime)) { + ok=false; + } + }; + if(ok == true) { + print('INFO: All members of replicaset are synchronized'); + quit(0); + } + sleep(1000); + } + print('ERROR: Members of replicaset are not synchronized'); + printjson(rs.status()); + quit(1);" + + mongo admin --host "${host}" -u admin -p "${MONGODB_ADMIN_PASSWORD}" --eval "${script}" +} + +# wait_replicaset_members waits till replset has specified number of members +function wait_replicaset_members() { + local host + host=$1 + local count + count=$2 + + local script + script="for (var i = 0; i < 60; i++) { + var ret = rs.status().members.length; + if (ret == ${count}) { + print('INFO: Replicaset has expected number of members'); + quit(0); + } + sleep(1000); + } + print('ERROR: Wrong count of members in replicaset'); + printjson(rs.status()); + quit(1);" + + mongo admin --host "${host}" -u admin -p "${MONGODB_ADMIN_PASSWORD}" --eval "${script}" +} diff --git a/help.md b/help.md new file mode 100644 index 0000000..e69de29