diff --git a/.env.sample b/.env.sample index b28f49a..8f8cc4a 100644 --- a/.env.sample +++ b/.env.sample @@ -2,10 +2,22 @@ TYPE=zulip DOMAIN=zulip.example.com -## Domain aliases -#EXTRA_DOMAINS=', `www.zulip.example.com`' - LETS_ENCRYPT_ENV=production SECRET_DB_PASSWORD_VERSION=v1 SECRET_RABBITMQ_PASSWORD_VERSION=v1 +SECRET_REDIS_PASSWORD_VERSION=v1 +SECRET_SMTP_PASSWORD_VERSION=v1 +SECRET_ZULIP_SECRET_VERSION=v1 +SECRET_MEMCACHED_PASSWORD_VERSION=v1 + +####################### +##### SMTP CONFIG ##### +####################### + +SETTING_EMAIL_HOST: "smtp.example.com" +SETTING_EMAIL_HOST_USER: "user" +SETTING_EMAIL_PORT: "587" # STARTTLS = 587 SSL/TLS = 465 + +#SETTING_EMAIL_USE_SSL: "False" # implicit SSL/TLS +SETTING_EMAIL_USE_TLS: "True" # STARTTLS diff --git a/README.md b/README.md index 29dd864..c6bc487 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ * **Category**: Apps -* **Status**: 0 +* **Status**: wip * **Image**: [`zulip`](https://hub.docker.com/r/zulip), 4, upstream -* **Healthcheck**: No +* **Healthcheck**: Yes * **Backups**: No * **Email**: No * **Tests**: No @@ -17,8 +17,12 @@ ## Quick start -* `abra app new zulip --secrets` +* `abra app new zulip` +* `abra app secret insert smtp_password v1 ` (A working SMTP server is required for setup) * `abra app config ` + * Populate SMTP settings by editing env variables that start with `SETTING_EMAIL` * `abra app deploy ` -For more, see [`docs.coopcloud.tech`](https://docs.coopcloud.tech). + + +For more, reed [`zulip's documentation`](https://zulip.readthedocs.io/en/latest/index.html). diff --git a/abra.sh b/abra.sh new file mode 100644 index 0000000..7725e75 --- /dev/null +++ b/abra.sh @@ -0,0 +1,5 @@ +export ENTRYPOINT_VERSION=v1 +export PG_BACKUP_VERSION=v1 +export MEM_ENTRYPOINT_VERSION=v1 +export REDIS_ENTRYPOINT_VERSION=v1 +export RABBIT_HEALTHCHECK_VERSION=v1 \ No newline at end of file diff --git a/compose.yml b/compose.yml index 2bbf494..b25c424 100644 --- a/compose.yml +++ b/compose.yml @@ -5,8 +5,11 @@ services: app: image: "zulip/docker-zulip:9.4-0" - ports: - - "80:80" + configs: + - source: entrypoint + target: /custom-entrypoint.sh + mode: 555 + entrypoint: /custom-entrypoint.sh environment: DB_HOST: "database" DB_HOST_PORT: "5432" @@ -15,19 +18,8 @@ services: SETTING_MEMCACHED_LOCATION: "memcached:11211" SETTING_RABBITMQ_HOST: "rabbitmq" SETTING_REDIS_HOST: "redis" - SECRETS_email_password: "123456789" - SECRETS_rabbitmq_password: "REPLACE_WITH_SECURE_RABBITMQ_PASSWORD" - SECRETS_postgres_password: "REPLACE_WITH_SECURE_POSTGRES_PASSWORD" - SECRETS_memcached_password: "REPLACE_WITH_SECURE_MEMCACHED_PASSWORD" - SECRETS_redis_password: "REPLACE_WITH_SECURE_REDIS_PASSWORD" - SECRETS_secret_key: "REPLACE_WITH_SECURE_SECRET_KEY" - SETTING_EXTERNAL_HOST: "localhost.localdomain" + SETTING_EXTERNAL_HOST: ${DOMAIN} SETTING_ZULIP_ADMINISTRATOR: "admin@example.com" - SETTING_EMAIL_HOST: "" - SETTING_EMAIL_HOST_USER: "noreply@example.com" - SETTING_EMAIL_PORT: "587" - SETTING_EMAIL_USE_SSL: "False" - SETTING_EMAIL_USE_TLS: "True" ZULIP_AUTH_BACKENDS: "EmailAuthBackend" volumes: - "zulip:/data:rw" @@ -56,13 +48,13 @@ services: configs: - source: pg_backup target: /pg_backup.sh - mode: 0555 + mode: 555 healthcheck: test: [ "CMD-SHELL", "pg_isready" ] interval: 10s timeout: 5s retries: 5 - + memcached: image: "memcached:alpine" command: @@ -73,35 +65,53 @@ services: echo "zulip@$$HOSTNAME:$$MEMCACHED_PASSWORD" > "$$MEMCACHED_SASL_PWDB" echo "zulip@localhost:$$MEMCACHED_PASSWORD" >> "$$MEMCACHED_SASL_PWDB" exec memcached -S + configs: + - source: memcached_entrypoint + target: /custom-entrypoint.sh + mode: 555 + entrypoint: /custom-entrypoint.sh + secrets: + - memcached_password environment: SASL_CONF_PATH: "/home/memcache/memcached.conf" MEMCACHED_SASL_PWDB: "/home/memcache/memcached-sasl-db" - MEMCACHED_PASSWORD: "REPLACE_WITH_SECURE_MEMCACHED_PASSWORD" rabbitmq: image: "rabbitmq:3.12.14" environment: RABBITMQ_DEFAULT_USER: "zulip" RABBITMQ_DEFAULT_PASS_FILE: "/run/secrets/rabbitmq_password" + configs: + - source: rabbitmq_healthcheck + target: /healthcheck.sh + mode: 555 secrets: - rabbitmq_password volumes: - "rabbitmq:/var/lib/rabbitmq:rw" + healthcheck: + test: [ "CMD-SHELL", "/healthcheck.sh" ] + interval: 10s + timeout: 5s + retries: 3 redis: image: "redis:alpine" + configs: + - source: redis_entrypoint + target: /custom-entrypoint.sh + mode: 555 + entrypoint: /custom-entrypoint.sh command: - "sh" - "-euc" - | echo "requirepass '$$REDIS_PASSWORD'" > /etc/redis.conf exec redis-server /etc/redis.conf - environment: - REDIS_PASSWORD: "REPLACE_WITH_SECURE_REDIS_PASSWORD" + environment: REDIS_PASSWORD volumes: - "redis:/data:rw" - secrets: db_password: name: ${STACK_NAME}_db_password_${SECRET_DB_PASSWORD_VERSION} @@ -109,11 +119,39 @@ secrets: rabbitmq_password: name: ${STACK_NAME}_rabbitmq_password_${SECRET_RABBITMQ_PASSWORD_VERSION} external: true + redis_password: + name: ${STACK_NAME}_redis_password_${SECRET_REDIS_PASSWORD_VERSION} + external: true + memcached_password: + name: ${STACK_NAME}_memcached_password_${SECRET_MEMCACHED_PASSWORD_VERSION} + external: true + smtp_password: + name: ${STACK_NAME}_smtp_password_${SECRET_SMTP_PASSWORD_VERSION} + external: true + zulip_secret: + name: ${STACK_NAME}_zulip_secret_${SECRET_ZULIP_SECRET_VERSION} + external: true + configs: pg_backup: name: ${STACK_NAME}_pg_backup_${PG_BACKUP_VERSION} file: pg_backup.sh + entrypoint: + name: ${STACK_NAME}_entrypoint_${ENTRYPOINT_VERSION} + file: entrypoint.sh.tmpl + template_driver: golang + memcached_entrypoint: + name: ${STACK_NAME}_memcached_entrypoint_${MEM_ENTRYPOINT_VERSION} + file: entrypoint.memcached.sh.tmpl + template_driver: golang + redis_entrypoint: + name: ${STACK_NAME}_redis_entrypoint_${REDIS_ENTRYPOINT_VERSION} + file: entrypoint.redis.sh.tmpl + template_driver: golang + rabbitmq_healthcheck: + name: ${STACK_NAME}_rabbitmq_healthcheck_${RABBIT_HEALTHCHECK_VERSION} + file: rabbitmq_healthcheck.sh volumes: zulip: @@ -121,6 +159,7 @@ volumes: rabbitmq: redis: + networks: internal: proxy: diff --git a/entryopoint.memcached.sh.tmpl b/entryopoint.memcached.sh.tmpl new file mode 100644 index 0000000..4a7c263 --- /dev/null +++ b/entryopoint.memcached.sh.tmpl @@ -0,0 +1,17 @@ +#!/bin/sh + +if [ -f /run/secrets/memcached_password ]; then + export MEMCACHED_PASSWORD=$(cat /run/secrets/memcached_password) +else + echo "Membcached password secret not found, skipping." +fi + + +set -e + +# first arg is `-f` or `--some-option` +if [ "${1#-}" != "$1" ]; then + set -- memcached "$@" +fi + +exec "$@" \ No newline at end of file diff --git a/entrypoint.redis.sh.tmpl b/entrypoint.redis.sh.tmpl new file mode 100644 index 0000000..5111577 --- /dev/null +++ b/entrypoint.redis.sh.tmpl @@ -0,0 +1,45 @@ +#!/bin/bash + +if [ -f /run/secrets/redis_password ]; then + export REDIS_PASSWORD=$(cat /run/secrets/redis_password) +else + echo "REDIS secret not found, skipping." +fi + + + + +### docker entrypoint script, for starting redis stack +BASEDIR=/opt/redis-stack +cd ${BASEDIR} + +CMD=${BASEDIR}/bin/redis-server +if [ -f /redis-stack.conf ]; then + CONFFILE=/redis-stack.conf +fi + +if [ -z "${REDIS_DATA_DIR}" ]; then + REDIS_DATA_DIR=/data +fi + +# when running in redis-stack (as opposed to redis-stack-server) +if [ -f ${BASEDIR}/nodejs/bin/node ]; then + ${BASEDIR}/nodejs/bin/node -r ${BASEDIR}/share/redisinsight/api/node_modules/dotenv/config ${BASEDIR}/share/redisinsight/api/dist/src/main.js dotenv_config_path=${BASEDIR}/share/redisinsight/.env & +fi + +if [ -z "${REDISEARCH_ARGS}" ]; then +REDISEARCH_ARGS="MAXSEARCHRESULTS 10000 MAXAGGREGATERESULTS 10000" +fi + +${CMD} \ +${CONFFILE} \ +--dir ${REDIS_DATA_DIR} \ +--protected-mode no \ +--daemonize no \ +--loadmodule /opt/redis-stack/lib/rediscompat.so \ +--loadmodule /opt/redis-stack/lib/redisearch.so ${REDISEARCH_ARGS} \ +--loadmodule /opt/redis-stack/lib/redistimeseries.so ${REDISTIMESERIES_ARGS} \ +--loadmodule /opt/redis-stack/lib/rejson.so ${REDISJSON_ARGS} \ +--loadmodule /opt/redis-stack/lib/redisbloom.so ${REDISBLOOM_ARGS} \ +--loadmodule /opt/redis-stack/lib/redisgears.so v8-plugin-path /opt/redis-stack/lib/libredisgears_v8_plugin.so ${REDISGEARS_ARGS} \ +${REDIS_ARGS} \ No newline at end of file diff --git a/entrypoint.sh.tmpl b/entrypoint.sh.tmpl new file mode 100644 index 0000000..daeb594 --- /dev/null +++ b/entrypoint.sh.tmpl @@ -0,0 +1,630 @@ +#!/bin/bash + +if [ -f /run/secrets/db_password ]; then + export SECRETS_postgres_password=$(cat /run/secrets/db_password) +else + echo "Postgres password secret not found, skipping." +fi + +if [ -f /run/secrets/memcached_password ]; then + export SECRETS_memcached_password=$(cat /run/secrets/memcached_password) +else + echo "Membcached password secret not found, skipping." +fi + +if [ -f /run/secrets/redis_password ]; then + export SECRETS_redis_password=$(cat /run/secrets/redis_password) +else + echo "DB password secret not found, skipping." +fi + +if [ -f /run/secrets/rabbitmq_password ]; then + export SECRETS_rabbitmq_password=$(cat /run/secrets/rabbitmq_password) +else + echo "DB password secret not found, skipping." +fi + +if [ -f /run/secrets/smtp_password ]; then + export SECRETS_email_password=$(cat /run/secrets/smtp_password) +else + echo "DB password secret not found, skipping." +fi + +if [ -f /run/secrets/zulip_secret ]; then + export SECRETS_secret_key=$(cat /run/secrets/zulip_secret) +else + echo "Zulip secret not found, skipping." +fi + + + + +if [ "$DEBUG" = "true" ] || [ "$DEBUG" = "True" ]; then + set -x + set -o functrace +fi +set -e +shopt -s extglob + +# DB aka Database +DB_HOST="${DB_HOST:-127.0.0.1}" +DB_HOST_PORT="${DB_HOST_PORT:-5432}" +DB_NAME="${DB_NAME:-zulip}" +DB_USER="${DB_USER:-zulip}" +REMOTE_POSTGRES_SSLMODE="${REMOTE_POSTGRES_SSLMODE:-prefer}" +# RabbitMQ +SETTING_RABBITMQ_HOST="${SETTING_RABBITMQ_HOST:-127.0.0.1}" +SETTING_RABBITMQ_USER="${SETTING_RABBITMQ_USER:-zulip}" +export RABBITMQ_NODE="$SETTING_RABBITMQ_HOST" +# Redis +SETTING_RATE_LIMITING="${SETTING_RATE_LIMITING:-True}" +SETTING_REDIS_HOST="${SETTING_REDIS_HOST:-127.0.0.1}" +SETTING_REDIS_PORT="${SETTING_REDIS_PORT:-6379}" +# Memcached +if [ -z "$SETTING_MEMCACHED_LOCATION" ]; then + SETTING_MEMCACHED_LOCATION="127.0.0.1:11211" +fi +# Nginx settings +DISABLE_HTTPS="${DISABLE_HTTPS:-false}" +NGINX_WORKERS="${NGINX_WORKERS:-2}" +NGINX_PROXY_BUFFERING="${NGINX_PROXY_BUFFERING:-off}" +NGINX_MAX_UPLOAD_SIZE="${NGINX_MAX_UPLOAD_SIZE:-80m}" +# Zulip certifcate parameters +SSL_CERTIFICATE_GENERATION="${SSL_CERTIFICATE_GENERATION:self-signed}" +# Zulip related settings +ZULIP_AUTH_BACKENDS="${ZULIP_AUTH_BACKENDS:-EmailAuthBackend}" +ZULIP_RUN_POST_SETUP_SCRIPTS="${ZULIP_RUN_POST_SETUP_SCRIPTS:-True}" +# Zulip user setup +FORCE_FIRST_START_INIT="${FORCE_FIRST_START_INIT:-False}" +# Auto backup settings +AUTO_BACKUP_ENABLED="${AUTO_BACKUP_ENABLED:-True}" +AUTO_BACKUP_INTERVAL="${AUTO_BACKUP_INTERVAL:-30 3 * * *}" +# Zulip configuration function specific variable(s) +SPECIAL_SETTING_DETECTION_MODE="${SPECIAL_SETTING_DETECTION_MODE:-}" +MANUAL_CONFIGURATION="${MANUAL_CONFIGURATION:-false}" +LINK_SETTINGS_TO_DATA="${LINK_SETTINGS_TO_DATA:-false}" +# entrypoint.sh specific variable(s) +SETTINGS_PY="/etc/zulip/settings.py" + +# BEGIN appRun functions +# === initialConfiguration === +prepareDirectories() { + mkdir -p "$DATA_DIR" "$DATA_DIR/backups" "$DATA_DIR/certs" "$DATA_DIR/letsencrypt" "$DATA_DIR/uploads" + [ -e /etc/letsencrypt ] || ln -ns "$DATA_DIR/letsencrypt" /etc/letsencrypt + echo "Preparing and linking the uploads folder ..." + rm -rf /home/zulip/uploads + ln -sfT "$DATA_DIR/uploads" /home/zulip/uploads + chown zulip:zulip -R "$DATA_DIR/uploads" + # Link settings folder + if [ "$LINK_SETTINGS_TO_DATA" = "True" ] || [ "$LINK_SETTINGS_TO_DATA" = "true" ]; then + # Create settings directories + if [ ! -d "$DATA_DIR/settings" ]; then + mkdir -p "$DATA_DIR/settings" + fi + if [ ! -d "$DATA_DIR/settings/etc-zulip" ]; then + cp -rf /etc/zulip "$DATA_DIR/settings/etc-zulip" + fi + # Link /etc/zulip/ settings folder + rm -rf /etc/zulip + ln -sfT "$DATA_DIR/settings/etc-zulip" /etc/zulip + fi + echo "Prepared and linked the uploads directory." +} +setConfigurationValue() { + if [ -z "$1" ]; then + echo "No KEY given for setConfigurationValue." + return 1 + fi + if [ -z "$3" ]; then + echo "No FILE given for setConfigurationValue." + return 1 + fi + local KEY="$1" + local VALUE + local FILE="$3" + local TYPE="$4" + if [ -z "$TYPE" ]; then + case "$2" in + [Tt][Rr][Uu][Ee]|[Ff][Aa][Ll][Ss][Ee]|[Nn]one) + TYPE="bool" + ;; + +([0-9])) + TYPE="integer" + ;; + [\[\(]*[\]\)]) + TYPE="array" + ;; + *) + TYPE="string" + ;; + esac + fi + case "$TYPE" in + emptyreturn) + if [ -z "$2" ]; then + return 0 + fi + ;; + literal) + VALUE="$1" + ;; + bool|boolean|int|integer|array) + VALUE="$KEY = $2" + ;; + string|*) + VALUE="$KEY = '${2//\'/\'}'" + ;; + esac + echo "$VALUE" >> "$FILE" + echo "Setting key \"$KEY\", type \"$TYPE\" in file \"$FILE\"." +} +nginxConfiguration() { + echo "Executing nginx configuration ..." + sed -i "s/worker_processes .*/worker_processes $NGINX_WORKERS;/g" /etc/nginx/nginx.conf + sed -i "s/client_max_body_size .*/client_max_body_size $NGINX_MAX_UPLOAD_SIZE;/g" /etc/nginx/nginx.conf + sed -i "s/proxy_buffering .*/proxy_buffering $NGINX_PROXY_BUFFERING;/g" /etc/nginx/zulip-include/proxy_longpolling + echo "Nginx configuration succeeded." +} +puppetConfiguration() { + echo "Executing puppet configuration ..." + + if [ "$DISABLE_HTTPS" == "True" ] || [ "$DISABLE_HTTPS" == "true" ]; then + echo "Disabling https in nginx." + crudini --set /etc/zulip/zulip.conf application_server http_only true + fi + if [ "$QUEUE_WORKERS_MULTIPROCESS" == "True" ] || [ "$QUEUE_WORKERS_MULTIPROCESS" == "true" ]; then + echo "Setting queue workers to run in multiprocess mode ..." + crudini --set /etc/zulip/zulip.conf application_server queue_workers_multiprocess true + elif [ "$QUEUE_WORKERS_MULTIPROCESS" == "False" ] || [ "$QUEUE_WORKERS_MULTIPROCESS" == "false" ]; then + echo "Setting queue workers to run in multithreaded mode ..." + crudini --set /etc/zulip/zulip.conf application_server queue_workers_multiprocess false + fi + + if [ -n "$LOADBALANCER_IPS" ]; then + echo "Setting IPs for load balancer" + crudini --set /etc/zulip/zulip.conf loadbalancer ips "${LOADBALANCER_IPS}" + fi + + /home/zulip/deployments/current/scripts/zulip-puppet-apply -f +} +configureCerts() { + case "$SSL_CERTIFICATE_GENERATION" in + self-signed) + GENERATE_SELF_SIGNED_CERT="True" + GENERATE_CERTBOT_CERT="False" + ;; + + certbot) + GENERATE_SELF_SIGNED_CERT="False" + GENERATE_CERTBOT_CERT="True" + ;; + *) + echo "Not requesting auto-generated self-signed certs." + GENERATE_CERTBOT_CERT="False" + GENERATE_SELF_SIGNED_CERT="False" + ;; + esac + if [ ! -e "$DATA_DIR/certs/zulip.key" ] && [ ! -e "$DATA_DIR/certs/zulip.combined-chain.crt" ]; then + + if [ "$GENERATE_CERTBOT_CERT" = "True" ]; then + # Zulip isn't yet running, so the certbot's challenge can't be met. + # We'll schedule this for later. + echo "Scheduling LetsEncrypt cert generation ..." + GENERATE_CERTBOT_CERT_SCHEDULED=True + + # Generate self-signed certs just to get Zulip going. + GENERATE_SELF_SIGNED_CERT=True + fi + + if [ "$GENERATE_SELF_SIGNED_CERT" = "True" ]; then + echo "Generating self-signed certificates ..." + mkdir -p "$DATA_DIR/certs" + /home/zulip/deployments/current/scripts/setup/generate-self-signed-cert "$SETTING_EXTERNAL_HOST" + mv /etc/ssl/private/zulip.key "$DATA_DIR/certs/zulip.key" + mv /etc/ssl/certs/zulip.combined-chain.crt "$DATA_DIR/certs/zulip.combined-chain.crt" + echo "Self-signed certificate generation succeeded." + else + echo "Certificates already exist. No need to generate them. Continuing." + fi + fi + if [ ! -e "$DATA_DIR/certs/zulip.key" ]; then + echo "SSL private key zulip.key is not present in $DATA_DIR." + echo "Certificates configuration failed." + echo "Consider setting SSL_CERTIFICATE_GENERATION in the environment to auto-generate" + exit 1 + fi + if [ ! -e "$DATA_DIR/certs/zulip.combined-chain.crt" ]; then + echo "SSL public key zulip.combined-chain.crt is not present in $DATA_DIR." + echo "Certificates configuration failed." + echo "Consider setting SSL_CERTIFICATE_GENERATION in the environment to auto-generate" + exit 1 + fi + ln -sfT "$DATA_DIR/certs/zulip.key" /etc/ssl/private/zulip.key + ln -sfT "$DATA_DIR/certs/zulip.combined-chain.crt" /etc/ssl/certs/zulip.combined-chain.crt + echo "Certificates configuration succeeded." +} +secretsConfiguration() { + echo "Setting Zulip secrets ..." + if [ ! -e "$DATA_DIR/zulip-secrets.conf" ]; then + echo "Generating Zulip secrets ..." + /root/zulip/scripts/setup/generate_secrets.py --production + mv "/etc/zulip/zulip-secrets.conf" "$DATA_DIR/zulip-secrets.conf" + ln -ns "$DATA_DIR/zulip-secrets.conf" "/etc/zulip/zulip-secrets.conf" + else + ln -nsf "$DATA_DIR/zulip-secrets.conf" "/etc/zulip/zulip-secrets.conf" + echo "Generating Zulip secrets ..." + /root/zulip/scripts/setup/generate_secrets.py --production + fi + echo "Secrets generation succeeded." + local key + for key in "${!SECRETS_@}"; do + [[ "$key" == SECRETS_*([0-9A-Z_a-z-]) ]] || continue + local SECRET_KEY="${key#SECRETS_}" + local SECRET_VAR="${!key}" + if [ -z "$SECRET_VAR" ]; then + echo "Empty secret for key \"$SECRET_KEY\"." + fi + crudini --set "$DATA_DIR/zulip-secrets.conf" "secrets" "${SECRET_KEY}" "${SECRET_VAR}" + done + echo "Zulip secrets configuration succeeded." +} +databaseConfiguration() { + echo "Setting database configuration ..." + setConfigurationValue "REMOTE_POSTGRES_HOST" "$DB_HOST" "$SETTINGS_PY" "string" + setConfigurationValue "REMOTE_POSTGRES_PORT" "$DB_HOST_PORT" "$SETTINGS_PY" "string" + setConfigurationValue "REMOTE_POSTGRES_SSLMODE" "$REMOTE_POSTGRES_SSLMODE" "$SETTINGS_PY" "string" + # The password will be set in secretsConfiguration + echo "Database configuration succeeded." +} +authenticationBackends() { + echo "Activating authentication backends ..." + local FIRST=true + local auth_backends + IFS=, read -r -a auth_backends <<< "$ZULIP_AUTH_BACKENDS" + for AUTH_BACKEND in "${auth_backends[@]}"; do + if [ "$FIRST" = true ]; then + setConfigurationValue "AUTHENTICATION_BACKENDS" "('zproject.backends.${AUTH_BACKEND//\'/\'}',)" "$SETTINGS_PY" "array" + FIRST=false + else + setConfigurationValue "AUTHENTICATION_BACKENDS += ('zproject.backends.${AUTH_BACKEND//\'/\'}',)" "" "$SETTINGS_PY" "literal" + fi + echo "Adding authentication backend \"$AUTH_BACKEND\"." + done + echo "Authentication backend activation succeeded." +} +zulipConfiguration() { + echo "Executing Zulip configuration ..." + if [ -n "$ZULIP_CUSTOM_SETTINGS" ]; then + echo -e "\n$ZULIP_CUSTOM_SETTINGS" >> "$SETTINGS_PY" + fi + local key + for key in "${!SETTING_@}"; do + [[ "$key" == SETTING_*([0-9A-Za-z_]) ]] || continue + local setting_key="${key#SETTING_}" + local setting_var="${!key}" + local type="string" + if [ -z "$setting_var" ]; then + echo "Empty var for key \"$setting_key\"." + continue + fi + # Zulip settings.py / zproject specific overrides here + if [ "$setting_key" = "AUTH_LDAP_CONNECTION_OPTIONS" ] || \ + [ "$setting_key" = "AUTH_LDAP_GLOBAL_OPTIONS" ] || \ + [ "$setting_key" = "AUTH_LDAP_USER_SEARCH" ] || \ + [ "$setting_key" = "AUTH_LDAP_GROUP_SEARCH" ] || \ + [ "$setting_key" = "AUTH_LDAP_REVERSE_EMAIL_SEARCH" ] || \ + [ "$setting_key" = "AUTH_LDAP_USER_ATTR_MAP" ] || \ + [ "$setting_key" = "AUTH_LDAP_USER_FLAGS_BY_GROUP" ] || \ + [ "$setting_key" = "AUTH_LDAP_GROUP_TYPE" ] || \ + [ "$setting_key" = "AUTH_LDAP_ADVANCED_REALM_ACCESS_CONTROL" ] || \ + [ "$setting_key" = "LDAP_SYNCHRONIZED_GROUPS_BY_REALM" ] || \ + [ "$setting_key" = "SOCIAL_AUTH_OIDC_ENABLED_IDPS" ] || \ + [ "$setting_key" = "SOCIAL_AUTH_SAML_ENABLED_IDPS" ] || \ + [ "$setting_key" = "SOCIAL_AUTH_SAML_ORG_INFO" ] || \ + { [ "$setting_key" = "LDAP_APPEND_DOMAIN" ] && [ "$setting_var" = "None" ]; } || \ + [ "$setting_key" = "SCIM_CONFIG" ] || \ + [ "$setting_key" = "SECURE_PROXY_SSL_HEADER" ] || \ + [[ "$setting_key" = "CSRF_"* ]] || \ + [ "$setting_key" = "REALM_HOSTS" ] || \ + [ "$setting_key" = "ALLOWED_HOSTS" ]; then + type="array" + fi + if [ "$SPECIAL_SETTING_DETECTION_MODE" = "True" ] || [ "$SPECIAL_SETTING_DETECTION_MODE" = "true" ] || \ + [ "$type" = "string" ]; then + type="" + fi + if [ "$setting_key" = "EMAIL_HOST_USER" ] || \ + [ "$setting_key" = "EMAIL_HOST_PASSWORD" ] || \ + [ "$setting_key" = "EXTERNAL_HOST" ]; then + type="string" + fi + setConfigurationValue "$setting_key" "$setting_var" "$SETTINGS_PY" "$type" + done + if ! su zulip -c "/home/zulip/deployments/current/manage.py checkconfig"; then + echo "Error in the Zulip configuration. Exiting." + exit 1 + fi + echo "Zulip configuration succeeded." +} +autoBackupConfiguration() { + if [ "$AUTO_BACKUP_ENABLED" != "True" ] && [ "$AUTO_BACKUP_ENABLED" != "true" ]; then + rm -f /etc/cron.d/autobackup + echo "Auto backup is disabled. Continuing." + return 0 + fi + printf 'MAILTO=""\n%s cd /;/sbin/entrypoint.sh app:backup\n' "$AUTO_BACKUP_INTERVAL" > /etc/cron.d/autobackup + echo "Auto backup enabled." +} +initialConfiguration() { + echo "=== Begin Initial Configuration Phase ===" + prepareDirectories + puppetConfiguration + nginxConfiguration + configureCerts + if [ "$MANUAL_CONFIGURATION" = "False" ] || [ "$MANUAL_CONFIGURATION" = "false" ]; then + # Start with the settings template file. + cp -a /home/zulip/deployments/current/zproject/prod_settings_template.py "$SETTINGS_PY" + databaseConfiguration + secretsConfiguration + authenticationBackends + zulipConfiguration + fi + autoBackupConfiguration + echo "=== End Initial Configuration Phase ===" +} +# === bootstrappingEnvironment === +waitingForDatabase() { + local TIMEOUT=60 + echo "Waiting for database server to allow connections ..." + while ! PGPASSWORD="${SECRETS_postgres_password?}" /usr/bin/pg_isready -h "$DB_HOST" -p "$DB_HOST_PORT" -U "$DB_USER" -t 1 >/dev/null 2>&1 + do + if ! ((TIMEOUT--)); then + echo "Could not connect to database server. Exiting." + exit 1 + fi + echo -n "." + sleep 1 + done +} +zulipFirstStartInit() { + echo "Executing Zulip first start init ..." + if [ -e "$DATA_DIR/.initiated" ] && [ "$FORCE_FIRST_START_INIT" != "True" ] && [ "$FORCE_FIRST_START_INIT" != "true" ]; then + echo "First Start Init not needed. Continuing." + return 0 + fi + local RETURN_CODE=0 + set +e + su zulip -c /home/zulip/deployments/current/scripts/setup/initialize-database + RETURN_CODE=$? + if [[ $RETURN_CODE != 0 ]]; then + echo "Zulip first start database initi failed in \"initialize-database\" exit code $RETURN_CODE. Exiting." + exit $RETURN_CODE + fi + set -e + touch "$DATA_DIR/.initiated" + echo "Zulip first start init sucessful." +} +zulipMigration() { + echo "Migrating Zulip to new version ..." + set +e + su zulip -c "/home/zulip/deployments/current/manage.py migrate --noinput" + local RETURN_CODE=$? + if [[ $RETURN_CODE != 0 ]]; then + echo "Zulip migration failed with exit code $RETURN_CODE. Exiting." + exit $RETURN_CODE + fi + set -e + rm -rf "$DATA_DIR/.zulip-*" + touch "$DATA_DIR/.zulip-$ZULIP_VERSION" + echo "Zulip migration succeeded." +} +runPostSetupScripts() { + echo "Post setup scripts execution ..." + if [ "$ZULIP_RUN_POST_SETUP_SCRIPTS" != "True" ] && [ "$ZULIP_RUN_POST_SETUP_SCRIPTS" != "true" ]; then + echo "Not running post setup scripts. ZULIP_RUN_POST_SETUP_SCRIPTS isn't true." + return 0 + fi + if [ ! -d "$DATA_DIR/post-setup.d/" ]; then + echo "No post-setup.d folder found. Continuing." + return 0 + fi + if [ ! "$(ls "$DATA_DIR/post-setup.d/")" ]; then + echo "No post setup scripts found in \"$DATA_DIR/post-setup.d/\"." + return 0 + fi + set +e + for file in "$DATA_DIR"/post-setup.d/*; do + if [ -x "$file" ]; then + echo "Executing \"$file\" ..." + bash -c "$file" + echo "Executed \"$file\". Return code $?." + else + echo "Permissions denied for \"$file\". Please check the permissions. Exiting." + exit 1 + fi + done + set -e + echo "Post setup scripts execution succeeded." +} +function runCertbotAsNeeded() { + if [ ! "$GENERATE_CERTBOT_CERT_SCHEDULED" = "True" ]; then + echo "Certbot is not scheduled to run." + return + fi + + echo "Waiting for nginx to come online before generating certbot certificate ..." + while ! curl -sk "$SETTING_EXTERNAL_HOST" >/dev/null 2>&1; do + sleep 1; + done + + echo "Generating LetsEncrypt/certbot certificate ..." + + # Remove the self-signed certs which were only needed to get Zulip going. + rm -f "$DATA_DIR"/certs/zulip.key "$DATA_DIR"/certs/zulip.combined-chain.crt + + ln -sf /sbin/certbot-deploy-hook /etc/letsencrypt/renewal-hooks/deploy/docker-deploy-hook + + # Accept the terms of service automatically. + /home/zulip/deployments/current/scripts/setup/setup-certbot \ + --agree-tos \ + --email="$SETTING_ZULIP_ADMINISTRATOR" \ + --skip-symlink \ + -- \ + "$SETTING_EXTERNAL_HOST" + + echo "LetsEncrypt cert generated." +} +bootstrappingEnvironment() { + echo "=== Begin Bootstrap Phase ===" + waitingForDatabase + zulipFirstStartInit + zulipMigration + runPostSetupScripts + # Hack: We run this in the background, since we need nginx to be + # started before we can create the certificate. See #142 for + # details on how we can clean this up. + runCertbotAsNeeded & + echo "=== End Bootstrap Phase ===" +} +# END appRun functions +# BEGIN app functions +appRun() { + initialConfiguration + bootstrappingEnvironment + echo "=== Begin Run Phase ===" + echo "Starting Zulip using supervisor with \"/etc/supervisor/supervisord.conf\" config ..." + echo "" + exec supervisord -n -c "/etc/supervisor/supervisord.conf" +} +appInit() { + echo "=== Running initial setup ===" + initialConfiguration + bootstrappingEnvironment +} +appManagePy() { + COMMAND="$1" + shift 1 + if [ -z "$COMMAND" ]; then + echo "No command given for manage.py. Defaulting to \"shell\"." + COMMAND="shell" + fi + echo "Running manage.py ..." + set +e + exec su zulip -c "/home/zulip/deployments/current/manage.py $(printf '%q ' "$COMMAND" "$@")" +} +appBackup() { + echo "Starting backup process ..." + local TIMESTAMP + TIMESTAMP=$(date -u -Iseconds | tr ':' '_') + if [ -d "/tmp/backup-$TIMESTAMP" ]; then + echo "Temporary backup folder for \"$TIMESTAMP\" already exists. Aborting." + echo "Backup process failed. Exiting." + exit 1 + fi + local BACKUP_FOLDER + BACKUP_FOLDER="/tmp/backup-$TIMESTAMP)" + mkdir -p "$BACKUP_FOLDER" + waitingForDatabase + pg_dump -h "$DB_HOST" -p "$DB_HOST_PORT" -U "$DB_USER" "$DB_NAME" > "$BACKUP_FOLDER/database-postgres.sql" + tar -zcvf "$DATA_DIR/backups/backup-$TIMESTAMP.tar.gz" "$BACKUP_FOLDER/" + rm -r "${BACKUP_FOLDER:?}/" + echo "Backup process succeeded." + exit 0 +} +appRestore() { + echo "Starting restore process ..." + if [ -z "$(ls -A "$DATA_DIR/backups/")" ]; then + echo "No backups to restore found in \"$DATA_DIR/backups/\"." + echo "Restore process failed. Exiting." + exit 1 + fi + while true; do + local backups=("$DATA_DIR"/backups/*) + printf '|-> %s\n' "${backups[@]#"$DATA_DIR"/backups/}" + echo "Please enter backup filename (full filename with extension): " + read -r BACKUP_FILE + if [ -z "$BACKUP_FILE" ]; then + echo "Empty filename given. Please try again." + echo "" + continue + fi + if [ ! -e "$DATA_DIR/backups/$BACKUP_FILE" ]; then + echo "File \"$BACKUP_FILE\" not found. Please try again." + echo "" + fi + break + done + echo "File \"$BACKUP_FILE\" found." + echo "" + echo "===============================================================" + echo "!! WARNING !! Your current data will be deleted!" + echo "!! WARNING !! YOU HAVE BEEN WARNED! You can abort with \"CTRL+C\"." + echo "!! WARNING !! Waiting 10 seconds before continuing ..." + echo "===============================================================" + echo "" + local TIMEOUT + for TIMEOUT in {10..1}; do + echo "$TIMEOUT" + sleep 1 + done + echo "!! WARNING !! Starting restore process ... !! WARNING !!" + waitingForDatabase + tar -zxvf "$DATA_DIR/backups/$BACKUP_FILE" -C /tmp + psql -h "$DB_HOST" -p "$DB_HOST_PORT" -U "$DB_USER" "$DB_NAME" < "/tmp/$(basename "$BACKUP_FILE" | cut -d. -f1)/database-postgres.sql" + rm -r "/tmp/$(basename "$BACKUP_FILE" | cut -d. -f1)/" + echo "Restore process succeeded. Exiting." + exit 0 +} +appCerts() { + configureCerts +} +appHelp() { + echo "Available commands:" + echo "> app:help - Show this help menu and exit" + echo "> app:version - Container Zulip server version" + echo "> app:managepy - Run Zulip's manage.py script (defaults to \"shell\")" + echo "> app:backup - Create backups of Zulip instances" + echo "> app:restore - Restore backups of Zulip instances" + echo "> app:certs - Create self-signed certificates" + echo "> app:run - Run the Zulip server" + echo "> app:init - Run inital setup of Zulip server" + echo "> [COMMAND] - Run given command with arguments in shell" +} +appVersion() { + echo "This container contains:" + echo "> Zulip server $ZULIP_VERSION" + echo "> Checksum: $ZULIP_CHECKSUM" + exit 0 +} +# END app functions + +case "$1" in + app:run) + appRun + ;; + app:init) + appInit + ;; + app:managepy) + shift 1 + appManagePy "$@" + ;; + app:backup) + appBackup + ;; + app:restore) + appRestore + ;; + app:certs) + appCerts + ;; + app:help) + appHelp + ;; + app:version) + appVersion + ;; + *) + exec "$@" || appHelp + ;; +esac \ No newline at end of file diff --git a/rabbitmq_healthcheck.sh b/rabbitmq_healthcheck.sh new file mode 100644 index 0000000..5f46826 --- /dev/null +++ b/rabbitmq_healthcheck.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -eo pipefail + +# A RabbitMQ node is considered healthy if all the below are true: +# * the rabbit app finished booting & it's running +# * there are no alarms +# * there is at least 1 active listener + +rabbitmqctl eval ' +{ true, rabbit_app_booted_and_running } = { rabbit:is_booted(node()), rabbit_app_booted_and_running }, +{ [], no_alarms } = { rabbit:alarms(), no_alarms }, +[] /= rabbit_networking:active_listeners(), +rabbitmq_node_is_healthy. +' || exit 1 \ No newline at end of file