diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8d454e8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +FROM registry.fedoraproject.org/fedora:25 +LABEL MAINTAINER SoftwareCollections.org + +ENV NAME=mongodb \ + VERSION=0 \ + RELEASE=1 \ + ARCH=x86_64 + +LABEL com.redhat.component = "$NAME" \ + name="$FGC/$NAME" \ + version="$VERSION" \ + release="$RELEASE.$DISTTAG" \ + architecture="$ARCH" \ + usage="docker run -d -e MONGODB_ADMIN_PASSWORD=my_pass $FGC/$NAME" \ + help="help.1" + +LABEL io.k8s.description="MongoDB is a scalable, high-performance, open source NoSQL database." \ + io.k8s.display-name="MongoDB 3.2" \ + io.openshift.expose-services="27017:mongodb" \ + io.openshift.tags="database,mongodb" + +ENV MONGODB_VERSION=3.2 \ + # Set paths to avoid hard-coding them in scripts. + HOME=/var/lib/mongodb \ + CONTAINER_SCRIPTS_PATH=/usr/share/container-scripts/mongodb + +EXPOSE 27017 + +ENTRYPOINT ["container-entrypoint"] +CMD ["run-mongod"] + +RUN INSTALL_PKGS="bind-utils gettext iproute rsync tar findutils python mongodb mongodb-server mongo-tools" && \ + dnf install -y --setopt=tsflags=nodocs $INSTALL_PKGS && \ + rpm -V $INSTALL_PKGS && \ + dnf clean all + +ADD root / + + +# Container setup +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/README.md b/README.md new file mode 120000 index 0000000..b2e53be --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +root/usr/share/container-scripts/mongodb/README.md \ No newline at end of file diff --git a/root/help.1 b/root/help.1 new file mode 100644 index 0000000..9285e28 --- /dev/null +++ b/root/help.1 @@ -0,0 +1,153 @@ +.\"t +.\" WARNING: Do not edit this file manually, it is generated from README.md automatically. +.\" +.\"t +.\" Automatically generated by Pandoc 1.16.0.2 +.\" +.TH "MONGODB\-32\-RHEL7" "1" "February 22, 2017" "Container Image Pages" "" +.hy +.SH MongoDB Docker image +.PP +This repository contains Dockerfiles for MongoDB images for general +usage and OpenShift. +Users can choose between RHEL and CentOS based images. +.SS Environment variables +.PP +The image recognizes the following environment variables that you can +set during initialization by passing \f[C]\-e\ VAR=VALUE\f[] to the +Docker run command. +.PP +.TS +tab(@); +l l. +T{ +Variable name +T}@T{ +Description +T} +_ +T{ +\f[C]MONGODB_USER\f[] +T}@T{ +User name for MONGODB account to be created +T} +T{ +\f[C]MONGODB_PASSWORD\f[] +T}@T{ +Password for the user account +T} +T{ +\f[C]MONGODB_DATABASE\f[] +T}@T{ +Database name +T} +T{ +\f[C]MONGODB_ADMIN_PASSWORD\f[] +T}@T{ +Password for the admin user +T} +.TE +.PP +The following environment variables influence the MongoDB configuration +file. +They are all optional. +.PP +.TS +tab(@); +lw(13.6n) lw(45.8n) lw(10.5n). +T{ +Variable name +T}@T{ +Description +T}@T{ +Default +T} +_ +T{ +\f[C]MONGODB_QUIET\f[] +T}@T{ +Runs MongoDB in a quiet mode that attempts to limit the amount of +output. +T}@T{ +true +T} +.TE +.PP +You can also set the following mount points by passing the +\f[C]\-v\ /host:/container\f[] flag to Docker. +.PP +.TS +tab(@); +l l. +T{ +Volume mount point +T}@T{ +Description +T} +_ +T{ +\f[C]/var/lib/mongodb/data\f[] +T}@T{ +MongoDB data directory +T} +.TE +.PP +\f[B]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.\f[] +.SS Usage +.PP +For this, we will assume that you are using the +\f[C]centos/mongodb\-32\-centos7\f[] image. +If you want to set only the mandatory environment variables and store +the database in the \f[C]/home/user/database\f[] directory on the host +filesystem, execute the following command: +.IP +.nf +\f[C] +$\ 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 +\f[] +.fi +.PP +If you are initializing the database and it\[aq]s the first time you are +using the specified shared volume, the database will be created with two +users: \f[C]admin\f[] and \f[C]MONGODB_USER\f[]. +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. +.SS Custom configuration file +.PP +It is allowed to use custom configuration file for mongod server. +Providing a custom configuration file supercedes the individual +configuration environment variable values. +.PP +To use custom configuration file in container it has to be mounted into +\f[C]/etc/mongod.conf\f[]. +For example to use configuration file stored in \f[C]/home/user\f[] +directory use this option for \f[C]docker\ run\f[] command: +\f[C]\-v\ /home/user/mongod.conf:/etc/mongod.conf:Z\f[]. +.PP +\f[B]Notice: Custom config file does not affect name of replica set. It +has to be set in \f[C]MONGODB_REPLICA_NAME\f[] environment variable.\f[] +.SS MongoDB admin user +.PP +The admin user name is set to \f[C]admin\f[] and you have to to specify +the password by setting the \f[C]MONGODB_ADMIN_PASSWORD\f[] environment +variable. +This process is done upon database initialization. +.SS Changing passwords +.PP +Since passwords are part of the image configuration, the only supported +method to change passwords for the database user (\f[C]MONGODB_USER\f[]) +and admin user is by changing the environment variables +\f[C]MONGODB_PASSWORD\f[] and \f[C]MONGODB_ADMIN_PASSWORD\f[], +respectively. +.PP +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. +.SH AUTHORS +Red Hat. diff --git a/root/usr/bin/cgroup-limits b/root/usr/bin/cgroup-limits new file mode 100755 index 0000000..b9d4edc --- /dev/null +++ b/root/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/root/usr/bin/container-entrypoint b/root/usr/bin/container-entrypoint new file mode 100755 index 0000000..9d8ad4d --- /dev/null +++ b/root/usr/bin/container-entrypoint @@ -0,0 +1,2 @@ +#!/bin/bash +exec "$@" diff --git a/root/usr/bin/run-mongod b/root/usr/bin/run-mongod new file mode 100755 index 0000000..e273e48 --- /dev/null +++ b/root/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/root/usr/bin/run-mongod-pet b/root/usr/bin/run-mongod-pet new file mode 120000 index 0000000..025fb1a --- /dev/null +++ b/root/usr/bin/run-mongod-pet @@ -0,0 +1 @@ +run-mongod-replication \ No newline at end of file diff --git a/root/usr/bin/run-mongod-replication b/root/usr/bin/run-mongod-replication new file mode 100755 index 0000000..fb803e1 --- /dev/null +++ b/root/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/root/usr/libexec/fix-permissions b/root/usr/libexec/fix-permissions new file mode 100755 index 0000000..6ad8d03 --- /dev/null +++ b/root/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/root/usr/share/container-scripts/mongodb/README.md b/root/usr/share/container-scripts/mongodb/README.md new file mode 100644 index 0000000..28b8988 --- /dev/null +++ b/root/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/root/usr/share/container-scripts/mongodb/common.sh b/root/usr/share/container-scripts/mongodb/common.sh new file mode 100644 index 0000000..5333d9b --- /dev/null +++ b/root/usr/share/container-scripts/mongodb/common.sh @@ -0,0 +1,259 @@ +#!/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 + echo >&2 "Cannot get address of replica set: no nodes are listed in service" + 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/root/usr/share/container-scripts/mongodb/init-replset.sh b/root/usr/share/container-scripts/mongodb/init-replset.sh new file mode 100755 index 0000000..7e60408 --- /dev/null +++ b/root/usr/share/container-scripts/mongodb/init-replset.sh @@ -0,0 +1,111 @@ +#!/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)" + +# Outputs available endpoints (hostnames) to stdout. +# This also includes hostname of the current pod. +# +# Uses the following global variables: +# - MONGODB_SERVICE_NAME (optional, defaults to 'mongodb') +function find_endpoints() { + local service_name="${MONGODB_SERVICE_NAME:-mongodb}" + + # Extract host names from lines like this: "10 33 0 mongodb-2.mongodb.myproject.svc.cluster.local." + dig "${service_name}" SRV +search +short | cut -d' ' -f4 | rev | cut -c2- | rev +} + +# 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 ..." + + # TODO: replace this with a call to `replset_addr` from common.sh, once it returns host names. + local endpoints + endpoints="$(find_endpoints | paste -s -d,)" + + if [ -z "${endpoints}" ]; then + info "ERROR: couldn't add host to replica set!" + info "CAUSE: DNS lookup for '${MONGODB_SERVICE_NAME:-mongodb}' returned no results." + return 1 + fi + + local replset_addr + replset_addr="${MONGODB_REPLICA_NAME}/${endpoints}" + + 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/root/usr/share/container-scripts/mongodb/mongodb.conf.template b/root/usr/share/container-scripts/mongodb/mongodb.conf.template new file mode 100644 index 0000000..e33ea21 --- /dev/null +++ b/root/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/root/usr/share/container-scripts/mongodb/scl_enable b/root/usr/share/container-scripts/mongodb/scl_enable new file mode 100644 index 0000000..5a25432 --- /dev/null +++ b/root/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/root/usr/share/container-scripts/mongodb/test-functions.sh b/root/usr/share/container-scripts/mongodb/test-functions.sh new file mode 100644 index 0000000..50ac0a0 --- /dev/null +++ b/root/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/test/run b/test/run new file mode 100755 index 0000000..a2f2a4e --- /dev/null +++ b/test/run @@ -0,0 +1,423 @@ +#!/bin/bash +# +# Test the MongoDB image. +# +# IMAGE_NAME specifies a name of the candidate image used for testing. +# The image has to be available before this script is executed. +# + +set -exo nounset +shopt -s nullglob + +IMAGE_NAME=${IMAGE_NAME-centos/mongodb-32-centos7-candidate} + +CIDFILE_DIR=$(mktemp --suffix=mongodb_test_cidfiles -d) + +function cleanup() { + for cidfile in $CIDFILE_DIR/* ; do + CONTAINER=$(cat $cidfile) + + echo "Stopping and removing container $CONTAINER..." + docker stop $CONTAINER + docker rm $CONTAINER + rm $cidfile + echo "Done." + done + rmdir $CIDFILE_DIR +} +trap cleanup EXIT SIGINT + +function get_cid() { + local id="$1" ; shift || return 1 + echo -n $(cat "$CIDFILE_DIR/$id") +} + +function get_container_ip() { + local id="$1" ; shift + docker inspect --format='{{.NetworkSettings.IPAddress}}' $(get_cid "$id") +} + +function mongo_cmd() { + docker run --rm $IMAGE_NAME mongo "$DB" --host $CONTAINER_IP -u "$USER" -p"$PASS" --eval "${@}" +} + +function mongo_admin_cmd() { + docker run --rm $IMAGE_NAME mongo admin --host $CONTAINER_IP -u admin -p"$ADMIN_PASS" --eval "${@}" +} + +# Make sure the invocation of docker run fails. +function assert_container_creation_fails() { + + # Time the docker run command. It should fail. If it doesn't fail, + # container will keep running so we kill it with SIGKILL to make sure + # timeout returns a non-zero value. + set +e + timeout -s 9 --preserve-status 10s docker run --rm "$@" $IMAGE_NAME + ret=$? + set -e + + # Timeout will exit with a high number. + if [ $ret -gt 30 ]; then + return 1 + fi +} + +function run_container_creation_tests() { + echo " Testing wrong user variables usage" + + assert_container_creation_fails -e MONGODB_USER=user -e MONGODB_PASSWORD=pass + assert_container_creation_fails -e MONGODB_DATABASE=db -e MONGODB_PASSWORD=pass + assert_container_creation_fails -e MONGODB_DATABASE=db -e MONGODB_USER=user + assert_container_creation_fails -e MONGODB_USER=user -e MONGODB_DATABASE=db -e MONGODB_PASSWORD=pass + + assert_container_creation_fails -e MONGODB_ADMIN_PASSWORD=Pass -e MONGODB_USER=user + assert_container_creation_fails -e MONGODB_ADMIN_PASSWORD=Pass -e MONGODB_PASSWORD=pass + assert_container_creation_fails -e MONGODB_ADMIN_PASSWORD=Pass -e MONGODB_DATABASE=db + + echo " Success!" + echo " Testing good user variables usage" + assert_container_creation_fails -e MONGODB_ADMIN_PASSWORD=Pass || [ $? -eq 1 ] + assert_container_creation_fails -e MONGODB_ADMIN_PASSWORD=Pass -e MONGODB_USER=user -e MONGODB_DATABASE=db -e MONGODB_PASSWORD=pass || [ $? -eq 1 ] + echo " Success!" +} + +function test_connection() { + local name=$1 ; shift + CONTAINER_IP=$(get_container_ip $name) + echo " Testing MongoDB connection to $CONTAINER_IP..." + local max_attempts=20 + local sleep_time=2 + for i in $(seq $max_attempts); do + echo " Trying to connect..." + set +e + mongo_cmd "db.getSiblingDB('test_database');" + status=$? + set -e + if [ $status -eq 0 ]; then + echo " Success!" + return 0 + fi + sleep $sleep_time + done + echo " Giving up: Failed to connect. Logs:" + docker logs $(get_cid $name) + return 1 +} + +test_scl_usage() { + local name="$1" + local run_cmd="$2" + local expected="$3" + + echo " Testing the image SCL enable" + out=$(docker run --rm ${IMAGE_NAME} /bin/bash -c "${run_cmd}") + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[/bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(get_cid $name) /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(get_cid $name) /bin/sh -ic "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/sh -ic "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi +} + +function test_mongo() { + echo " Testing MongoDB" + if [ -v ADMIN_PASS ]; then + echo " Testing Admin user privileges" + mongo_admin_cmd "db=db.getSiblingDB('${DB}');db.dropUser('${USER}');" + mongo_admin_cmd "db=db.getSiblingDB('${DB}');db.createUser({user:'${USER}',pwd:'${PASS}',roles:['readWrite','userAdmin','dbAdmin']});" + mongo_admin_cmd "db=db.getSiblingDB('${DB}');db.testData.insert({x:0});" + mongo_cmd "db.createUser({user:'test_user2',pwd:'test_password2',roles:['readWrite']});" + fi + echo " Testing user privileges" + mongo_cmd "db.testData.insert({ y : 1 });" + mongo_cmd "db.testData.insert({ z : 2 });" + mongo_cmd "db.testData.find().forEach(printjson);" + mongo_cmd "db.testData.count();" + mongo_cmd "db.testData.drop();" + mongo_cmd "db.dropDatabase();" + echo " Success!" +} + +function test_config_option() { + local env_var=$1 ; shift + local env_val=$1 ; shift + local config_part=$1 ; shift + + local name="configuration_${env_var}" + + # If $value is a string, it needs to be in simple quotes ''. + DOCKER_ARGS=" +-e MONGODB_DATABASE=db +-e MONGODB_USER=user +-e MONGODB_PASSWORD=password +-e MONGODB_ADMIN_PASSWORD=adminPassword +-e $env_var=${env_val} +" + create_container ${name} ${DOCKER_ARGS} + + # need to set these because `mongo_cmd` relies on global variables + USER=user + PASS=password + DB=db + + test_connection ${name} + + # If nothing is found, grep returns 1 and test fails. + docker exec $(get_cid $name) bash -c "cat /etc/mongod.conf" | grep -q "${config_part}" + + docker stop $(get_cid ${name}) +} + +function run_configuration_tests() { + echo " Testing image configuration settings" + test_config_option MONGODB_QUIET true "quiet: true" + echo " Success!" +} + +wait_for_cid() { + local max_attempts=10 + local sleep_time=1 + local attempt=1 + local result=1 + while [ $attempt -le $max_attempts ]; do + [ -f $cid_file ] && [ -s $cid_file ] && break + echo "Waiting for container start..." + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done +} + +function create_container() { + local name=$1 ; shift + local cargs=${CONTAINER_ARGS:-} + cid_file="$CIDFILE_DIR/$name" + # create container with a cidfile in a directory for cleanup + docker run --cidfile $cid_file -d $cargs "$@" $IMAGE_NAME + echo "Created container $(cat $cid_file)" + wait_for_cid +} + +function assert_login_access() { + local USER=$1 ; shift + local PASS=$1 ; shift + local success=$1 ; shift + + if mongo_cmd 'db.version()' ; then + if $success ; then + echo " $USER($PASS) access granted as expected" + return + fi + else + if ! $success ; then + echo " $USER($PASS) access denied as expected" + return + fi + fi + echo " $USER($PASS) login assertion failed" + exit 1 +} + +function run_tests() { + local name=$1 ; shift + envs="-e MONGODB_USER=$USER -e MONGODB_PASSWORD=$PASS -e MONGODB_DATABASE=$DB" + if [ -v ADMIN_PASS ]; then + envs="$envs -e MONGODB_ADMIN_PASSWORD=$ADMIN_PASS" + fi + create_container $name $envs + CONTAINER_IP=$(get_container_ip $name) + test_connection $name + echo " Testing scl usage" + test_scl_usage $name 'mongo --version' '3.2' + test_mongo $name +} + +function run_change_password_test() { + local name="change_password" + + local database='db' + local user='user' + local password='password' + local admin_password='adminPassword' + + local volume_dir + volume_dir=`mktemp -d --tmpdir mongodb-testdata.XXXXX` + chmod a+rwx ${volume_dir} + + DOCKER_ARGS=" +-e MONGODB_DATABASE=${database} +-e MONGODB_USER=${user} +-e MONGODB_PASSWORD=${password} +-e MONGODB_ADMIN_PASSWORD=${admin_password} +-v ${volume_dir}:/var/lib/mongodb/data:Z +" + create_container ${name} ${DOCKER_ARGS} + + # need to set these because `mongo_cmd` relies on global variables + USER=${user} + PASS=${password} + DB=${database} + + # need this to wait for the container to start up + CONTAINER_IP=$(get_container_ip ${name}) + test_connection ${name} + + echo " Testing login" + + assert_login_access ${user} ${password} true + DB='admin' assert_login_access 'admin' ${admin_password} true + + echo " Changing passwords" + + docker stop $(get_cid ${name}) + DOCKER_ARGS=" +-e MONGODB_DATABASE=${database} +-e MONGODB_USER=${user} +-e MONGODB_PASSWORD=NEW_${password} +-e MONGODB_ADMIN_PASSWORD=NEW_${admin_password} +-v ${volume_dir}:/var/lib/mongodb/data:Z +" + create_container "${name}_NEW" ${DOCKER_ARGS} + + # need to set this because `mongo_cmd` relies on global variables + PASS="NEW_${password}" + + # need this to wait for the container to start up + CONTAINER_IP=$(get_container_ip "${name}_NEW") + test_connection "${name}_NEW" + + echo " Testing login with new passwords" + + assert_login_access ${user} "NEW_${password}" true + assert_login_access ${user} ${password} false + + DB='admin' assert_login_access 'admin' "NEW_${admin_password}" true + DB='admin' assert_login_access 'admin' ${admin_password} false + + # need to remove volume_dir with sudo because of permissions of files written + # by the Docker container + sudo rm -rf ${volume_dir} + + echo " Success!" +} + +function run_mount_config_test() { + local name="mount_config" + echo " Testing config file mount" + local database='db' + local user='user' + local password='password' + local admin_password='adminPassword' + + local volume_dir + volume_dir=`mktemp -d --tmpdir mongodb-testdata.XXXXX` + chmod a+rwx ${volume_dir} + config_file=$volume_dir/mongod.conf + echo "dbpath=/var/lib/mongodb/dbpath +unixSocketPrefix = /var/lib/mongodb" > $config_file + chmod a+r ${config_file} + + DOCKER_ARGS=" +-e MONGODB_DATABASE=${database} +-e MONGODB_USER=${user} +-e MONGODB_PASSWORD=${password} +-e MONGODB_ADMIN_PASSWORD=${admin_password} +-v ${config_file}:/etc/mongod.conf:Z +-v ${volume_dir}:/var/lib/mongodb/dbpath:Z +" + create_container ${name} ${DOCKER_ARGS} + + # need to set these because `mongo_cmd` relies on global variables + USER=${user} + PASS=${password} + DB=${database} + + # need this to wait for the container to start up + CONTAINER_IP=$(get_container_ip ${name}) + echo " Testing mongod is running" + test_connection ${name} + echo " Testing config file works" + docker exec $(get_cid ${name}) bash -c "test -S /var/lib/mongodb/mongodb-27017.sock" + + # need to remove volume_dir with sudo because of permissions of files written + # by the Docker container + sudo rm -rf ${volume_dir} + + echo " Success!" +} + +run_doc_test() { + local tmpdir=$(mktemp -d) + local f + echo " Testing documentation in the container image" + # Extract the help files from the container + for f in /usr/share/container-scripts/mongodb/README.md help.1 ; do + docker run --rm ${IMAGE_NAME} /bin/bash -c "cat /${f}" >${tmpdir}/$(basename ${f}) + # Check whether the files include some important information + for term in MONGODB_ADMIN_PASSWORD volume ; do + if ! cat ${tmpdir}/$(basename ${f}) | grep -q -e "${term}" ; then + echo "ERROR: File /${f} does not include '${term}'." + return 1 + fi + done + done + # Check whether the files use the correct format + if ! file ${tmpdir}/help.1 | grep -q roff ; then + echo "ERROR: /help.1 is not in troff or groff format" + return 1 + fi + echo " Success!" + echo +} + +run_WT_cache_test() { + local name="WT_cache_size" + echo " Testing setting of WT cache size" + local database='db' + local user='user' + local password='password' + local admin_password='adminPassword' + + DOCKER_ARGS=" +-e MONGODB_DATABASE=${database} +-e MONGODB_USER=${user} +-e MONGODB_PASSWORD=${password} +-e MONGODB_ADMIN_PASSWORD=${admin_password} +" + # need to set these because `mongo_cmd` relies on global variables + USER=${user} + PASS=${password} + DB=${database} + ADMIN_PASS=${admin_password} + + # minimum is 1G + create_container "${name}_200M" ${DOCKER_ARGS} -m 200M + test_connection ${name}_200M + mongo_admin_cmd "if (db.serverStatus()['wiredTiger']['cache']['maximum bytes configured'] == Math.pow(2,30)){quit(0)}; quit(1)" + # if greater that 1G, use 60% of (RAM - 1G) + create_container "${name}_6G" ${DOCKER_ARGS} -m 6G + test_connection ${name}_6G + mongo_admin_cmd "if (db.serverStatus()['wiredTiger']['cache']['maximum bytes configured'] == 3*Math.pow(2,30)){quit(0)}; quit(1)" + + echo " Success!" + echo +} + + +# Tests. +run_container_creation_tests +run_configuration_tests +USER="user1" PASS="pass1" DB="test_db" ADMIN_PASS="r00t" run_tests admin +# Test with random uid in container +CONTAINER_ARGS="-u 12345" USER="user1" PASS="pass1" DB="test_db" ADMIN_PASS="r00t" run_tests admin_altuid +run_change_password_test +run_mount_config_test +run_doc_test +run_WT_cache_test diff --git a/test/run-openshift b/test/run-openshift new file mode 100755 index 0000000..56264dd --- /dev/null +++ b/test/run-openshift @@ -0,0 +1,88 @@ +#!/bin/bash +# +# Test the MongoDB image in OpenShift. +# +# IMAGE_NAME specifies a name of the candidate image used for testing. +# The image has to be available before this script is executed. +# + +set -exo nounset + +function prepare_oc_client() { + # Prepare oc command + if [[ ! -d ../openshift-client/ ]]; then + OC_RELEASE=$(curl -s https://api.github.com/repos/openshift/origin/releases | grep "browser_download_url" | grep "openshift-origin-client" | grep "linux-64bit" | sort | tail -n 1 | sed -e 's|"browser_download_url": ||' | tr -d ' "') + + curl -L ${OC_RELEASE} | tar xz + mv ./openshift-origin-client-tools-* ../openshift-client + fi + export PATH=${PATH}:`pwd`/../openshift-client/ +} +prepare_oc_client + +function cleanup() { + echo "Stopping and removing OpenShift cluster..." + oc delete project --all + oc cluster down +} +trap cleanup EXIT SIGINT + +function print_logs() { + oc get all + while read pod_info; do + pod=$(echo ${pod_info} | tr -s ' ' | cut -f1 -d' ') + echo "INFO: printing logs for pod ${pod}" + oc logs ${pod} + done < <(oc get pods --no-headers=true) +} +trap print_logs ERR + +# Start OpenShift cluster +[ -d openshift.pv ] || mkdir openshift.pv +oc cluster up --host-pv-dir=$(pwd)/openshift.pv + +# Prepare image - add tag different than :latest +# With :latest default imagePullPolicy is Always +# https://docs.openshift.com/enterprise/3.2/dev_guide/managing_images.html#image-pull-policy +docker tag ${IMAGE_NAME} ${IMAGE_NAME}:test-openshift +IMAGE_NAME=${IMAGE_NAME}:test-openshift + +# +# General functions +# + +# wait_for_ready_pods wait till number of pods with label get ready +function wait_for_ready_pods() { + local label="$1" + local count="$2" + for i in $(seq 30); do + if [[ "$(oc get pods --no-headers=true -l${label} | grep "1/1" | wc -l)" -eq ${count} ]]; then + return 0 + fi + echo "Waiting for ${count} ready pods labeled with '${label}'..." + sleep 20 + done + return 1 +} + +# Deploy MongoDB clustered application +USER=user +PASS=pass +ADMIN_PASS=adminPass +DB=db + +oc new-project petset-example +oc new-app --file=../examples/petset/mongodb-petset-persistent.yaml -p MONGODB_USER=${USER} -p MONGODB_PASSWORD=${PASS} -p MONGODB_DATABASE=${DB} -p MONGODB_ADMIN_PASSWORD=${ADMIN_PASS} -p MONGODB_IMAGE=${IMAGE_NAME} -p VOLUME_CAPACITY=500M + +wait_for_ready_pods "app=mongodb-petset-replication" 3 +wait_for_ready_pods "openshift.io/deployer-pod-for.name" 0 + +host="rs0/$(oc get endpoints mongodb --no-headers | tr -s ' ' | cut -f2 -d' ')" + +docker exec origin kubectl run --attach --restart=Never mongodb-test --image ${IMAGE_NAME} --env="MONGODB_ADMIN_PASSWORD=${ADMIN_PASS}" --command -- bash -c "set -x +. /usr/share/container-scripts/mongodb/common.sh +. /usr/share/container-scripts/mongodb/test-functions.sh +wait_for_mongo_up '${host}' +wait_replicaset_members '${host}' 3 +insert_and_wait_for_replication '${host}' '{a:5, b:10}'" +