diff --git a/README.md b/README.md index e6a66709..f18368b6 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,30 @@ This project is deployed in accordance to the [DargStack template](https://githu ## Table of Contents - 1. [secrets](#secrets) + 1. [x-shared](#x-shared) - 2. [services](#services) + 2. [secrets](#secrets) - 3. [volumes](#volumes) + 3. [services](#services) + + 4. [volumes](#volumes) + + +## x-shared + + + - ### `zammad-service` + + You can access the helpdesk at [zammad.app.localhost](https://zammad.app.localhost/). ## secrets + - ### `elasticsearch_password` + + The search engine's password for the default user. + - ### `grafana_admin_email` The observation dashboard's admin email. @@ -197,6 +211,10 @@ This project is deployed in accordance to the [DargStack template](https://githu You can check the database connector's setup logs using `portainer`. + - ### `elasticsearch` + + You cannot access the search engine via a web interface. + - ### `geoip` You cannot access the ip geolocator via a web interface. @@ -209,6 +227,10 @@ This project is deployed in accordance to the [DargStack template](https://githu You cannot access the jobber via a web interface. + - ### `memcached` + + You cannot access the caching system via a web interface. + - ### `minio` ![development](https://img.shields.io/badge/-development-informational.svg?style=flat-square) You can access the s3 console at [minio.app.localhost](https://minio.app.localhost/). @@ -246,6 +268,10 @@ This project is deployed in accordance to the [DargStack template](https://githu You can access reccoom's database via `adminer`. + - ### `redis` + + You cannot access the caching system via a web interface. + - ### `redpanda` You can access the event streaming platform's ui as described under `redpanda-console`. @@ -274,6 +300,30 @@ This project is deployed in accordance to the [DargStack template](https://githu You can access the main project's frontend at [app.localhost](https://app.localhost/). + - ### `zammad-backup` + + You cannot access the helpdesk backup service via a web interface. + + - ### `zammad-init` + + You cannot access the helpdesk initialization service via a web interface. + + - ### `zammad-nginx` + + You can access the helpdesk at [zammad.app.localhost](https://zammad.app.localhost/). + + - ### `zammad-railsserver` + + You cannot access the helpdesk application server directly. + + - ### `zammad-scheduler` + + You cannot access the helpdesk scheduler directly. + + - ### `zammad-websocket` + + You cannot access the helpdesk websocket server directly. + ## volumes @@ -294,6 +344,10 @@ This project is deployed in accordance to the [DargStack template](https://githu The change data capture's logs. + - ### `elasticsearch_data` + + The search engine's data. + - ### `grafana_data` The observation dashboard's data. @@ -322,6 +376,10 @@ This project is deployed in accordance to the [DargStack template](https://githu The recommendation database's data. + - ### `redis_data` + + The caching system's data. + - ### `redpanda_data` The message queue's data. @@ -330,4 +388,12 @@ This project is deployed in accordance to the [DargStack template](https://githu The frontend's data. + - ### `zammad-backup_data` + + The helpdesk backup's data. + + - ### `zammad_data` + + The helpdesk's data. + diff --git a/src/development/certificates/mkcert.sh b/src/development/certificates/mkcert.sh index b17c4055..25c841c4 100755 --- a/src/development/certificates/mkcert.sh +++ b/src/development/certificates/mkcert.sh @@ -39,4 +39,5 @@ create "traefik" \ `# redpanda` "redpanda.app.localhost" \ `# traefik` "traefik.app.localhost" \ `# tusd` "tusd.app.localhost" \ - `# vibetype` "app.localhost" "www.app.localhost" "127.0.0.1" "0.0.0.0" \ No newline at end of file + `# vibetype` "app.localhost" "www.app.localhost" "127.0.0.1" "0.0.0.0" \ + `# zammad` "zammad.app.localhost" \ No newline at end of file diff --git a/src/development/secrets/elasticsearch/password.secret b/src/development/secrets/elasticsearch/password.secret new file mode 100644 index 00000000..f3f85cae --- /dev/null +++ b/src/development/secrets/elasticsearch/password.secret @@ -0,0 +1 @@ +elastic \ No newline at end of file diff --git a/src/development/secrets/elasticsearch/password.secret.template b/src/development/secrets/elasticsearch/password.secret.template new file mode 100644 index 00000000..a6bb01c1 --- /dev/null +++ b/src/development/secrets/elasticsearch/password.secret.template @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/development/stack.yml b/src/development/stack.yml index f64f6755..6ba7b44c 100644 --- a/src/development/stack.yml +++ b/src/development/stack.yml @@ -3,7 +3,34 @@ # Vibetype # https://github.com/maevsi/vibetype/ --- +x-shared: + zammad-service: + &zammad-service # You can access the helpdesk at [zammad.app.localhost](https://zammad.app.localhost/). + environment: &zammad-environment + ELASTICSEARCH_HOST: elasticsearch + ELASTICSEARCH_SCHEMA: https + ELASTICSEARCH_USER: elastic + MEMCACHE_SERVERS: memcached:11211 + POSTGRESQL_DB_CREATE: "false" + POSTGRESQL_DB: zammad + POSTGRESQL_HOST: postgres + POSTGRESQL_OPTIONS: ?pool=50 + REDIS_URL: redis://redis:6379 + image: ghcr.io/zammad/zammad:6.5.2-90 + secrets: + - source: elasticsearch_password + target: /run/environment-variables/ELASTICSEARCH_PASS + - source: postgres_role_service_zammad_username + target: /run/environment-variables/POSTGRESQL_USER + - source: postgres_role_service_zammad_password + target: /run/environment-variables/POSTGRESQL_PASS + volumes: + - zammad_data:/opt/zammad/storage + - ../production/configurations/zammad/docker-entrypoint.sh:/docker-entrypoint.sh:ro secrets: + elasticsearch_password: + # The search engine's password for the default user. + file: ./secrets/elasticsearch/password.secret grafana_admin_email: # The observation dashboard's admin email. file: ./secrets/grafana/admin_email.secret @@ -166,6 +193,20 @@ services: - postgres_user volumes: - ../production/configurations/debezium-postgres-connector/entrypoint.sh:/entrypoint.sh:ro + elasticsearch: + # You cannot access the search engine via a web interface. + environment: + discovery.type: single-node + network.publish_host: elasticsearch + ELASTIC_PASSWORD_FILE: /run/secrets/elasticsearch_password + image: elasticsearch:8.19.11 + secrets: + - source: elasticsearch_password + uid: "1000" + gid: "1000" + mode: 0o400 + volumes: + - elasticsearch_data:/usr/share/elasticsearch/data geoip: # You cannot access the ip geolocator via a web interface. image: ghcr.io/observabilitystack/geoip-api:2026-02 @@ -220,6 +261,10 @@ services: volumes: - ../production/backups/postgres/:/backups/ - ./configurations/jobber/.jobber:/home/jobberuser/.jobber:ro + memcached: + # You cannot access the caching system via a web interface. + image: memcached:1.6.40-alpine + # command: memcached -m 256M minio: #DARGSTACK-REMOVE # You can access the s3 console at [minio.app.localhost](https://minio.app.localhost/). # You can access the s3 api service at [s3.app.localhost](https://s3.app.localhost/) if you want to access via cli from outside the stack. @@ -404,6 +449,11 @@ services: - postgres_user volumes: - reccoom_postgres_data:/var/lib/postgresql/ + redis: + # You cannot access the caching system via a web interface. + image: redis:7.4.7-alpine + volumes: + - redis_data:/data redpanda: # You can access the event streaming platform's ui as described under `redpanda-console`. command: @@ -573,6 +623,48 @@ services: - ../../../vibetype/:/srv/app/ #DARGSTACK-REMOVE - vibetype_data:/srv/app/node_modules #DARGSTACK-REMOVE - ./configurations/postgraphile/jwtRS256.key.pub:/run/environment-variables/NUXT_PUBLIC_VIO_AUTH_JWT_PUBLIC_KEY:ro + zammad-backup: + # You cannot access the helpdesk backup service via a web interface. + <<: *zammad-service + command: ["zammad-backup"] + user: 0:0 + volumes: + - zammad-backup_data:/var/tmp/zammad + - zammad_data:/opt/zammad/storage:ro + - ../production/configurations/zammad/docker-entrypoint.sh:/docker-entrypoint.sh:ro + zammad-init: + # You cannot access the helpdesk initialization service via a web interface. + <<: *zammad-service + command: ["zammad-init"] + # depends_on: + # - zammad-postgresql + user: 0:0 + zammad-nginx: + # You can access the helpdesk at [zammad.app.localhost](https://zammad.app.localhost/). + <<: *zammad-service + command: ["zammad-nginx"] + deploy: + labels: + - traefik.enable=true + - traefik.http.routers.zammad.entryPoints=web + - traefik.http.routers.zammad.middlewares=redirectscheme #DARGSTACK-REMOVE + - traefik.http.routers.zammad.rule=Host(`zammad.${STACK_DOMAIN}`) + - traefik.http.routers.zammad_secure.entryPoints=web-secure + - traefik.http.routers.zammad_secure.rule=Host(`zammad.${STACK_DOMAIN}`) + - traefik.http.routers.zammad_secure.tls.options=mintls13@file #DARGSTACK-REMOVE + - traefik.http.services.zammad.loadbalancer.server.port=8080 + zammad-railsserver: + # You cannot access the helpdesk application server directly. + <<: *zammad-service + command: ["zammad-railsserver"] + zammad-scheduler: + # You cannot access the helpdesk scheduler directly. + <<: *zammad-service + command: ["zammad-scheduler"] + zammad-websocket: + # You cannot access the helpdesk websocket server directly. + <<: *zammad-service + command: ["zammad-websocket"] version: "3.7" volumes: debezium_kafka_configuration: @@ -584,6 +676,9 @@ volumes: debezium_kafka_logs: # The change data capture's logs. {} + elasticsearch_data: + # The search engine's data. + {} grafana_data: # The observation dashboard's data. {} @@ -591,7 +686,7 @@ volumes: # The s3 server's data. {} pnpm_data: - # The node package manager's data. + # The node package manager's data. {} portainer_data: # The container manager's data. @@ -605,9 +700,18 @@ volumes: reccoom_postgres_data: # The recommendation database's data. {} + redis_data: + # The caching system's data. + {} redpanda_data: # The message queue's data. {} vibetype_data: # The frontend's data. {} + zammad-backup_data: + # The helpdesk backup's data. + {} + zammad_data: + # The helpdesk's data. + {} diff --git a/src/production/configurations/zammad/docker-entrypoint.sh b/src/production/configurations/zammad/docker-entrypoint.sh new file mode 100755 index 00000000..1459d9b7 --- /dev/null +++ b/src/production/configurations/zammad/docker-entrypoint.sh @@ -0,0 +1,211 @@ +#!/usr/bin/env bash + +set -e + +# START of maevsi entrypoint script customization +ENVIRONMENT_VARIABLES_PATH="/run/environment-variables" + +is_valid_var_name() { + case "$1" in + *[!a-zA-Z0-9_]*|'') return 1 ;; + *) return 0 ;; + esac +} + +load_env_file() { + file="$1" + name=$(basename "$file") + is_valid_var_name "$name" || return 0 + value="$(cat "$file")" + export "$name=$value" +} + +load_environment_variables() { + [ -d "$ENVIRONMENT_VARIABLES_PATH" ] || return 0 + set -- "$ENVIRONMENT_VARIABLES_PATH"/* + [ -e "$1" ] || return 0 + + for file in "$ENVIRONMENT_VARIABLES_PATH"/*; do + [ -f "$file" ] && load_env_file "$file" + done +} + +load_environment_variables +# END of maevsi entrypoint script customization + +: "${AUTOWIZARD_JSON:=''}" +: "${AUTOWIZARD_RELATIVE_PATH:='tmp/auto_wizard.json'}" +: "${ELASTICSEARCH_ENABLED:=true}" +: "${ELASTICSEARCH_HOST:=zammad-elasticsearch}" +: "${ELASTICSEARCH_PORT:=9200}" +: "${ELASTICSEARCH_SCHEMA:=http}" +: "${ELASTICSEARCH_NAMESPACE:=zammad}" +: "${ELASTICSEARCH_REINDEX:=true}" +: "${NGINX_PORT:=8080}" +: "${NGINX_CLIENT_MAX_BODY_SIZE:=50M}" +: "${NGINX_SERVER_NAME:=_}" +: "${NGINX_SERVER_SCHEME:=\$scheme}" +: "${POSTGRESQL_DB:=zammad_production}" +: "${POSTGRESQL_DB_CREATE:=true}" +: "${POSTGRESQL_HOST:=zammad-postgresql}" +: "${POSTGRESQL_PORT:=5432}" +: "${POSTGRESQL_USER:=zammad}" +: "${POSTGRESQL_PASS:=zammad}" +: "${POSTGRESQL_OPTIONS:=}" +: "${RAILS_ENV:=production}" +: "${RAILS_LOG_TO_STDOUT:=true}" +: "${RAILS_TRUSTED_PROXIES:=127.0.0.1,::1}" +: "${ZAMMAD_DIR:=/opt/zammad}" +: "${ZAMMAD_RAILSSERVER_HOST:=zammad-railsserver}" +: "${ZAMMAD_RAILSSERVER_PORT:=3000}" +: "${ZAMMAD_WEBSOCKET_HOST:=zammad-websocket}" +: "${ZAMMAD_WEBSOCKET_PORT:=6042}" + +# Support both ZAMMAD_WEB_CONCURRENCY (as recommended by the Zammad docker stack & documentation) +# and WEB_CONCURRENCY (Zammad and Rails default). +: "${ZAMMAD_WEB_CONCURRENCY:=0}" +: "${WEB_CONCURRENCY:=${ZAMMAD_WEB_CONCURRENCY}}" +export WEB_CONCURRENCY + +ESCAPED_POSTGRESQL_PASS=$(echo "$POSTGRESQL_PASS" | sed -e 's/[\/&]/\\&/g') +export DATABASE_URL="postgres://${POSTGRESQL_USER}:${ESCAPED_POSTGRESQL_PASS}@${POSTGRESQL_HOST}:${POSTGRESQL_PORT}/${POSTGRESQL_DB}${POSTGRESQL_OPTIONS}" + +function check_zammad_ready { + # Verify that migrations have been ran and seeds executed to process ENV vars like FQDN correctly. + until bundle exec rails r 'ActiveRecord::Migration.check_all_pending!; Translation.any? || raise' &> /dev/null; do + echo "waiting for init container to finish install or update..." + sleep 2 + done +} + +# zammad init +if [ "$1" = 'zammad-init' ]; then + # install / update zammad + until (echo > /dev/tcp/"${POSTGRESQL_HOST}"/"${POSTGRESQL_PORT}") &> /dev/null; do + echo "waiting for postgresql server to be ready..." + sleep 1 + done + + # check if database exists / update to new version + echo "initialising / updating database..." + if ! (bundle exec rails r 'puts User.any?' 2> /dev/null | grep -q true); then + if [ "${POSTGRESQL_DB_CREATE}" == "true" ]; then + bundle exec rake db:create db:migrate db:seed + else + bundle exec rake db:migrate db:seed + fi + + # create autowizard.json on first install + if base64 -d <<< "${AUTOWIZARD_JSON}" &>> /dev/null; then + echo "Saving autowizard json payload..." + base64 -d <<< "${AUTOWIZARD_JSON}" > "${AUTOWIZARD_RELATIVE_PATH}" + fi + else + echo Clearing cache... + bundle exec rails r "Rails.cache.clear" + + echo Executing migrations... + bundle exec rake db:migrate + + echo Synchronizing locales and translations... + bundle exec rails r "Locale.sync; Translation.sync" + fi + + # es config + echo "changing settings..." + if [ "${ELASTICSEARCH_ENABLED}" == "false" ]; then + bundle exec rails r "Setting.set('es_url', '')" + else + bundle exec rails r "Setting.set('es_url', '${ELASTICSEARCH_SCHEMA}://${ELASTICSEARCH_HOST}:${ELASTICSEARCH_PORT}'); Setting.set('es_index', '${ELASTICSEARCH_NAMESPACE}')" + + if [ -n "${ELASTICSEARCH_USER}" ] && [ -n "${ELASTICSEARCH_PASS}" ]; then + bundle exec rails r "Setting.set('es_user', \"${ELASTICSEARCH_USER}\"); Setting.set('es_password', \"${ELASTICSEARCH_PASS}\")" + fi + + until (echo > /dev/tcp/"${ELASTICSEARCH_HOST}/${ELASTICSEARCH_PORT}") &> /dev/null; do + echo "zammad-init waiting for elasticsearch server to be ready…" + sleep 1 + done + + if [ "${ELASTICSEARCH_REINDEX}" == "true" ]; then + if bundle exec rails r "SearchIndexBackend.index_exists?('Ticket') || exit(1)" + then + echo "Elasticsearch index exists, no automatic reindexing is needed." + else + echo "Elasticsearch index does not exist yet, create it now…" + bundle exec rake zammad:searchindex:rebuild + fi + fi + fi + +# zammad nginx +elif [ "$1" = 'zammad-nginx' ]; then + check_zammad_ready + + # configure nginx + + # Ensure that nginx has a short TTL so that recreated containers with new IP addresses are found. + NAMESERVER=$(grep "^nameserver" --max-count 1 < /etc/resolv.conf | awk '{print $2}') + echo "resolver $NAMESERVER valid=5s;" > /etc/nginx/conf.d/resolver.conf + + # Inject docker related settings into the nginx configuration. + # + # There is a workaround needed to support DNS resolution of upstream container names with short TTL: + # we set the proxy pass directly with a variable including the URL (!), rather than just referring to the + # upstream {} definition. For details, see https://tenzer.dk/nginx-with-dynamic-upstreams/. + sed -e "s#\(listen\)\(.*\)80#\1\2${NGINX_PORT}#g" \ + -e "s#proxy_set_header X-Forwarded-Proto .*;#proxy_set_header X-Forwarded-Proto ${NGINX_SERVER_SCHEME};#g" \ + -e "s#proxy_pass http://zammad-railsserver;#set \$zammad_railsserver_url http://${ZAMMAD_RAILSSERVER_HOST}:${ZAMMAD_RAILSSERVER_PORT}; proxy_pass \$zammad_railsserver_url;#g" \ + -e "s#proxy_pass http://zammad-websocket;#set \$zammad_websocket_url http://${ZAMMAD_WEBSOCKET_HOST}:${ZAMMAD_WEBSOCKET_PORT}; proxy_pass \$zammad_websocket_url;#g" \ + -e "s#server_name .*#server_name ${NGINX_SERVER_NAME};#g" \ + -e "s#client_max_body_size .*#client_max_body_size ${NGINX_CLIENT_MAX_BODY_SIZE};#g" \ + -e 's#/var/log/nginx/zammad.\(access\|error\).log#/dev/stdout#g' < contrib/nginx/zammad.conf > /etc/nginx/sites-enabled/default + + # + # Once we can use an nginx version >= 1.27.3, we can drop the proxy_pass workaround above and + # use the new dedicated syntax for configuring resolver usage on the upstream definitions directly: + # + #-e "s#server .*:3000#server ${ZAMMAD_RAILSSERVER_HOST}:${ZAMMAD_RAILSSERVER_PORT} resolve#g" \ + #-e "s#server .*:6042#server ${ZAMMAD_WEBSOCKET_HOST}:${ZAMMAD_WEBSOCKET_PORT} resolve#g" \ + + echo "starting nginx..." + + exec /usr/sbin/nginx -g 'daemon off;' + +# zammad-railsserver +elif [ "$1" = 'zammad-railsserver' ]; then + check_zammad_ready + + echo "starting railsserver... with WEB_CONCURRENCY=${WEB_CONCURRENCY}" + + #shellcheck disable=SC2101 + exec bundle exec puma -b tcp://[::]:"${ZAMMAD_RAILSSERVER_PORT}" -e "${RAILS_ENV}" + +# zammad-scheduler +elif [ "$1" = 'zammad-scheduler' ]; then + check_zammad_ready + + echo "starting background services..." + + exec bundle exec script/background-worker.rb start + +# zammad-websocket +elif [ "$1" = 'zammad-websocket' ]; then + check_zammad_ready + + echo "starting websocket server..." + + exec bundle exec script/websocket-server.rb -b 0.0.0.0 -p "${ZAMMAD_WEBSOCKET_PORT}" start + +# zammad-backup +elif [ "$1" = 'zammad-backup' ]; then + check_zammad_ready + + echo "starting backup..." + + exec contrib/docker/backup.sh + +# Pass all other container commands to shell +else + exec "$@" +fi