diff --git a/03_container_layer/docker/admin/rocketchat/.dockerignore b/03_container_layer/docker/admin/rocketchat/.dockerignore new file mode 100644 index 0000000..900b393 --- /dev/null +++ b/03_container_layer/docker/admin/rocketchat/.dockerignore @@ -0,0 +1,5 @@ +.env +*.env +*.key +*.pem +README.md diff --git a/03_container_layer/docker/admin/rocketchat/.env.example b/03_container_layer/docker/admin/rocketchat/.env.example new file mode 100644 index 0000000..187b496 --- /dev/null +++ b/03_container_layer/docker/admin/rocketchat/.env.example @@ -0,0 +1,5 @@ +RC_BASE_URL=http://localhost:3000 +RC_ADMIN_USER=rc-admin +RC_ADMIN_PASS=Admin1234! +RC_ADMIN_EMAIL=admin@range42.local +HTTP_PORT=3000 diff --git a/03_container_layer/docker/admin/rocketchat/Dockerfile b/03_container_layer/docker/admin/rocketchat/Dockerfile new file mode 100644 index 0000000..551f027 --- /dev/null +++ b/03_container_layer/docker/admin/rocketchat/Dockerfile @@ -0,0 +1,28 @@ +# +# ISSUE 147 +# + +# +# BUILDER — node:20-alpine (same base as official Rocket.Chat Dockerfile) +# +FROM node:20-alpine AS builder + +RUN apk add --no-cache jq && \ + wget -q https://github.com/mikefarah/yq/releases/download/v4.44.1/yq_linux_amd64 \ + -O /usr/bin/yq && chmod +x /usr/bin/yq + +COPY provisioning/ /provisioning/ +RUN chmod +x /provisioning/init.sh + +# +# RUNTIME — rocketchat/rocket.chat:latest (Alpine/Node-based) +# +FROM rocketchat/rocket.chat:latest AS runtime + +USER root + +COPY --from=builder /usr/bin/yq /usr/bin/yq +COPY --from=builder /usr/bin/jq /usr/bin/jq +COPY --from=builder /provisioning/ /provisioning/ + +ENTRYPOINT ["/provisioning/init.sh"] diff --git a/03_container_layer/docker/admin/rocketchat/Makefile b/03_container_layer/docker/admin/rocketchat/Makefile new file mode 100644 index 0000000..f1c3063 --- /dev/null +++ b/03_container_layer/docker/admin/rocketchat/Makefile @@ -0,0 +1,91 @@ +# +# ISSUE 147 +# + +SERVICE = rocketchat +PROVISIONER = rocketchat-provisioner +DEBUG_SERVICE = $(SERVICE)-debug + +.PHONY: help up down stop stop-debug-build build rebuild build-up rebuild-up tokens reprovision term term-debug-build clean print + +help: + @echo "" + @echo "" + @echo " Available : " + @echo "" + @echo " make up - run $(SERVICE) stack in background" + @echo " make down - down $(SERVICE) stack" + @echo " make stop - stop $(SERVICE) " + @echo " make stop-debug-build - stop $(DEBUG_SERVICE) " + @echo "" + @echo " make build - build $(SERVICE) image" + @echo " make build-up - build $(SERVICE) image then run" + @echo "" + @echo " make rebuild - full rebuild $(SERVICE) without cache" + @echo " make rebuild-up - full rebuild $(SERVICE) without cache then run" + @echo "" + @echo " make tokens - print generated personal access tokens" + @echo " make term - open shell in $(SERVICE) container" + @echo " make term-debug-build - build and get term on $(DEBUG_SERVICE) " + @echo " make clean - delete all containers, images, volumes and unused network" + @echo " make print - print this help" + @echo "" + @echo "" + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +up: + docker compose up -d $(SERVICE) +down: + docker compose down +stop: + docker compose stop $(SERVICE) # stop only - main + +stop-debug-build : + docker compose stop $(DEBUG_SERVICE) # stop only debug + docker compose rm -sf $(DEBUG_SERVICE) + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +build: + docker compose build + +rebuild: + docker compose down + docker compose build --no-cache + +# # # # # # # # # # # # + +build-up: + docker compose build + docker compose up -d + +rebuild-up: + docker compose down + docker compose build --no-cache + docker compose up -d + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +tokens: + docker run --rm -v rocketchat-tokens:/tokens alpine cat /tokens/tokens.txt + +reprovision: + docker run --rm -v rocketchat-tokens:/tokens alpine rm -f /tokens/.provisioned + docker compose up $(PROVISIONER) + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +clean: + docker compose down -v --rmi all + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +term: + docker exec -it $(SERVICE) /bin/sh + +term-debug-build: + docker compose up --build -d $(DEBUG_SERVICE) + docker exec -it $(DEBUG_SERVICE) /bin/sh + +print: help diff --git a/03_container_layer/docker/admin/rocketchat/README.md b/03_container_layer/docker/admin/rocketchat/README.md new file mode 100644 index 0000000..40bcec1 --- /dev/null +++ b/03_container_layer/docker/admin/rocketchat/README.md @@ -0,0 +1,169 @@ +# Rocket.Chat — Standalone Docker Deployment + +Issue 147. Dockerized Rocket.Chat with MongoDB replica set and automated user / personal-access-token provisioning. + +--- + +## Prerequisites + +| Requirement | Version | +|---|---| +| Docker | 24+ | +| Docker Compose (plugin) | v2.20+ | +| `make` | any | + +--- + +## Quick Start + +```sh +# 1. Copy and edit environment file +cp .env.example .env +$EDITOR .env + +# 2. Build and start the full stack +make build-up + +# 3. Wait for the provisioner to finish, then check tokens +make tokens +``` + +> **MongoDB replica set** initialises automatically via the `mongo-init-replica` one-shot container. No manual `rs.initiate()` step is needed. + +The web UI is available at `http://localhost:3000` (or `RC_BASE_URL`). +Default admin credentials: `rc-admin` / `Admin1234!` (change in `.env`). + +--- + +## Build & Push + +```sh +# Build provisioner image +make build + +# Full rebuild without cache +make rebuild + +# Tag and push (adjust registry as needed) +docker tag rocketchat-provisioner registry.example.com/range42/rocketchat-provisioner:latest +docker push registry.example.com/range42/rocketchat-provisioner:latest +``` + +--- + +## Declaring Users + +Edit `provisioning/users.yml` before first deployment: + +```yaml +# !! CHANGE ALL PASSWORDS BEFORE DEPLOYING !! + +admins: + - username: rc-admin2 + email: admin2@range42.local + password: "Admin1234!" + name: "RC Admin 2" + +users: + - username: trainee01 + email: trainee01@range42.local + password: "Trainee1234!" + name: "Trainee 01" +``` + +The primary admin (`rc-admin`) is created automatically via the `ADMIN_USERNAME` environment variable. The users listed in `users.yml` are **additional** accounts. + +--- + +## Token Retrieval + +Personal access tokens are written to `/tokens/tokens.txt` (inside the `rocketchat-tokens` volume) at the end of provisioning. + +```sh +# Print all tokens +make tokens + +# Or read directly from the volume +docker run --rm -v rocketchat_rocketchat-tokens:/tokens:ro busybox cat /tokens/tokens.txt +``` + +Each line has the format: + +``` +username:personalAccessToken +``` + +--- + +## API Usage Examples + +```sh +# Get server info (no auth needed) +curl http://localhost:3000/api/v1/info + +# List channels (authenticated) +TOKEN="" +USER_ID="" + +curl -X GET http://localhost:3000/api/v1/channels.list \ + -H "X-Auth-Token: ${TOKEN}" \ + -H "X-User-Id: ${USER_ID}" + +# Post a message +curl -X POST http://localhost:3000/api/v1/chat.postMessage \ + -H "X-Auth-Token: ${TOKEN}" \ + -H "X-User-Id: ${USER_ID}" \ + -H "Content-Type: application/json" \ + -d '{"channel":"#general","text":"Hello from range42!"}' +``` + +--- + +## Environment Variables + +| Variable | Default | Description | +|---|---|---| +| `RC_BASE_URL` | `http://localhost:3000` | Public URL (used by Rocket.Chat as `ROOT_URL`) | +| `RC_ADMIN_USER` | `rc-admin` | Initial admin username | +| `RC_ADMIN_PASS` | `Admin1234!` | Initial admin password | +| `RC_ADMIN_EMAIL` | `admin@range42.local` | Initial admin email | +| `HTTP_PORT` | `3000` | Host port mapped to Rocket.Chat | + +--- + +## Troubleshooting + +### MongoDB replica set issues + +**Symptom:** Rocket.Chat exits with `MongoServerError: not primary`. + +**Cause:** The replica set has not been initialised yet. The `mongo-init-replica` container handles this automatically, but it requires MongoDB to be healthy first. + +**Fix:** +```sh +# Check mongo-init-replica logs +docker logs rocketchat-mongo-init + +# Force re-init manually if needed +docker exec rocketchat-mongodb mongosh --eval \ + "rs.initiate({ _id: 'rs0', members: [{ _id: 0, host: 'mongodb:27017' }] })" +``` + +### Provisioner exits with auth error + +**Symptom:** `[init] ERROR: Failed to authenticate as admin.` + +**Cause:** Rocket.Chat is not yet fully ready (it can take 60–90 s on first boot). + +**Fix:** The provisioner will be restarted automatically by Docker Compose if `restart: "no"` is overridden, or you can re-run it manually: +```sh +docker compose run --rm provisioner +``` + +### Tokens already exist + +If the provisioner runs a second time, `generatePersonalAccessToken` will fail for tokens named `api-token` that already exist. This is handled gracefully — a warning is printed and the existing token is left in place. Re-provision with a clean volume to regenerate: +```sh +docker volume rm rocketchat_rocketchat-tokens +docker compose run --rm provisioner +``` diff --git a/03_container_layer/docker/admin/rocketchat/compose.yml b/03_container_layer/docker/admin/rocketchat/compose.yml new file mode 100644 index 0000000..15d92a7 --- /dev/null +++ b/03_container_layer/docker/admin/rocketchat/compose.yml @@ -0,0 +1,96 @@ +# +# ISSUE 147 +# + +services: + + mongodb: + image: mongo:6.0 + container_name: rocketchat-mongodb + command: mongod --replSet rs0 --oplogSize 128 + volumes: + - rocketchat-mongodb-data:/data/db + healthcheck: + test: ["CMD", "mongosh", "--eval", "try { rs.status().ok } catch(e) { 0 }"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 20s + restart: unless-stopped + + mongo-init-replica: + image: mongo:6.0 + container_name: rocketchat-mongo-init + command: > + bash -c " + until mongosh mongodb:27017/admin --eval 'db.adminCommand(\"ping\")' >/dev/null 2>&1; do sleep 2; done; + mongosh mongodb:27017/admin --eval ' + try { rs.status().ok } catch(e) { + rs.initiate({ _id: \"rs0\", members: [{ _id: 0, host: \"mongodb:27017\" }] }) + }'; + until mongosh mongodb:27017/admin --eval 'rs.isMaster().ismaster' 2>/dev/null | grep -q true; do sleep 2; done; + echo \"Replica set PRIMARY ready\" + " + depends_on: + mongodb: + condition: service_healthy + restart: "no" + + rocketchat: &main + image: rocketchat/rocket.chat:latest + container_name: rocketchat + environment: + MONGO_URL: "mongodb://mongodb:27017/rocketchat?replicaSet=rs0" + MONGO_OPLOG_URL: "mongodb://mongodb:27017/local?replicaSet=rs0" + ROOT_URL: ${RC_BASE_URL:-http://localhost:3000} + PORT: "3000" + DEPLOY_PLATFORM: docker + ADMIN_USERNAME: ${RC_ADMIN_USER:-rc-admin} + ADMIN_PASS: ${RC_ADMIN_PASS:-Admin1234!} + ADMIN_EMAIL: ${RC_ADMIN_EMAIL:-admin@range42.local} + OVERWRITE_SETTING_Show_Setup_Wizard: completed + volumes: + - rocketchat-uploads:/app/uploads + ports: + - "${HTTP_PORT:-3000}:3000" + depends_on: + mongodb: + condition: service_healthy + mongo-init-replica: + condition: service_completed_successfully + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/api/v1/info"] + interval: 20s + timeout: 10s + retries: 15 + start_period: 60s + restart: unless-stopped +# +# #### #### do not remove. +# +# debug: +# <<: *main +# container_name: rocketchat-debug +# command: ["sleep", "infinity"] + + provisioner: + build: + context: . + dockerfile: Dockerfile + container_name: rocketchat-provisioner + environment: + RC_URL: http://rocketchat:3000 + RC_ADMIN_USER: ${RC_ADMIN_USER:-rc-admin} + RC_ADMIN_PASS: ${RC_ADMIN_PASS:-Admin1234!} + USERS_FILE: /provisioning/users.yml + volumes: + - rocketchat-tokens:/tokens + depends_on: + rocketchat: + condition: service_healthy + restart: "no" + +volumes: + rocketchat-mongodb-data: + rocketchat-uploads: + rocketchat-tokens: diff --git a/03_container_layer/docker/admin/rocketchat/provisioning/init.sh b/03_container_layer/docker/admin/rocketchat/provisioning/init.sh new file mode 100644 index 0000000..1b7f14f --- /dev/null +++ b/03_container_layer/docker/admin/rocketchat/provisioning/init.sh @@ -0,0 +1,221 @@ +#!/usr/bin/env sh +# +# ISSUE 147 +# +# Rocket.Chat provisioner: creates users and personal access tokens. +# Reads: $USERS_FILE (YAML with admins[] and users[] lists) +# Writes: /tokens/tokens.txt (username:token per line) +# + +set -eu + +RC_URL="${RC_URL:-http://rocketchat:3000}" +RC_ADMIN_USER="${RC_ADMIN_USER:-rc-admin}" +RC_ADMIN_PASS="${RC_ADMIN_PASS:-Admin1234!}" +USERS_FILE="${USERS_FILE:-/provisioning/users.yml}" +TOKENS_FILE="/tokens/tokens.txt" +STAMP_FILE="/tokens/.provisioned" + +# ───────────────────────────────────────────────────────────────────────────── +# 1. Wait for Rocket.Chat health (max 180s, 60 × 3s) +# ───────────────────────────────────────────────────────────────────────────── +echo "[init] Waiting for Rocket.Chat at ${RC_URL} ..." +attempts=0 +max_attempts=60 + +until curl -sf "${RC_URL}/api/v1/info" >/dev/null 2>&1; do + attempts=$((attempts + 1)) + if [ "${attempts}" -ge "${max_attempts}" ]; then + echo "[init] ERROR: Rocket.Chat did not become healthy after $((max_attempts * 3))s. Aborting." + exit 1 + fi + echo "[init] Waiting ... (${attempts}/${max_attempts})" + sleep 3 +done + +echo "[init] Rocket.Chat is up." + +# ───────────────────────────────────────────────────────────────────────────── +# 2. Idempotency stamp +# ───────────────────────────────────────────────────────────────────────────── +if [ -f "${STAMP_FILE}" ]; then + echo "[init] Already provisioned (${STAMP_FILE} exists). Exiting." + exit 0 +fi + +mkdir -p /tokens +: > "${TOKENS_FILE}" + +# ───────────────────────────────────────────────────────────────────────────── +# 3. Login as admin — capture auth token and user ID +# ───────────────────────────────────────────────────────────────────────────── +echo "[init] Logging in as ${RC_ADMIN_USER} ..." +auth_resp=$(curl -sf -X POST "${RC_URL}/api/v1/login" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg u "${RC_ADMIN_USER}" --arg p "${RC_ADMIN_PASS}" \ + '{"username":$u,"password":$p}')") + +rc_admin_token=$(echo "${auth_resp}" | jq -r '.data.authToken') +rc_admin_id=$(echo "${auth_resp}" | jq -r '.data.userId') + +if [ -z "${rc_admin_token}" ] || [ "${rc_admin_token}" = "null" ]; then + echo "[init] ERROR: Failed to authenticate as admin. Check RC_ADMIN_USER / RC_ADMIN_PASS." + exit 1 +fi + +echo "[init] Admin auth OK (userId=${rc_admin_id})." + +# ───────────────────────────────────────────────────────────────────────────── +# Helper: create a user via REST API +# Usage: create_user +# ───────────────────────────────────────────────────────────────────────────── +create_user() { + _username="$1" + _email="$2" + _password="$3" + _name="$4" + _roles="$5" + + echo "[init] Creating user: ${_username} ..." + _payload=$(jq -n \ + --arg u "${_username}" \ + --arg e "${_email}" \ + --arg p "${_password}" \ + --arg n "${_name}" \ + --argjson r "${_roles}" \ + '{"username":$u,"email":$e,"password":$p,"name":$n, + "roles":$r,"joinDefaultChannels":true, + "sendWelcomeEmail":false,"verified":true}') + + _resp=$(curl -sf -X POST "${RC_URL}/api/v1/users.create" \ + -H "X-Auth-Token: ${rc_admin_token}" \ + -H "X-User-Id: ${rc_admin_id}" \ + -H "Content-Type: application/json" \ + -d "${_payload}" 2>&1) || true + + # tolerate "Username is already in use" (idempotent) + _success=$(echo "${_resp}" | jq -r '.success // false') + _error=$(echo "${_resp}" | jq -r '.error // ""') + + if [ "${_success}" = "true" ]; then + echo "[init] User ${_username} created." + elif echo "${_error}" | grep -qi "already in use\|already exists\|duplicate"; then + echo "[init] User ${_username} already exists — skipping." + else + echo "[init] WARNING: Unexpected response for ${_username}: ${_resp}" + fi +} + +# ───────────────────────────────────────────────────────────────────────────── +# Helper: generate personal access token for a user +# The user must authenticate as themselves (tokens are user-owned in RC). +# Usage: generate_token +# ───────────────────────────────────────────────────────────────────────────── +generate_token() { + _username="$1" + _password="$2" + + echo "[token] Generating PAT for ${_username} ..." + + # Login as the user + _user_auth=$(curl -sf -X POST "${RC_URL}/api/v1/login" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg u "${_username}" --arg p "${_password}" \ + '{"username":$u,"password":$p}')") || true + + _user_token=$(echo "${_user_auth}" | jq -r '.data.authToken // ""') + _user_id=$(echo "${_user_auth}" | jq -r '.data.userId // ""') + + if [ -z "${_user_token}" ] || [ "${_user_token}" = "null" ]; then + echo "[token] WARNING: Could not log in as ${_username} — skipping token generation." + return + fi + + # Generate personal access token (tokenName must be unique per user) + _token_resp=$(curl -sf -X POST "${RC_URL}/api/v1/users.generatePersonalAccessToken" \ + -H "X-Auth-Token: ${_user_token}" \ + -H "X-User-Id: ${_user_id}" \ + -H "Content-Type: application/json" \ + -d '{"tokenName":"api-token"}') || true + + _pat=$(echo "${_token_resp}" | jq -r '.token // "ERROR"') + + if [ "${_pat}" = "ERROR" ] || [ -z "${_pat}" ]; then + # May already exist — try to list and skip gracefully + echo "[token] WARNING: Could not generate PAT for ${_username} (may already exist)." + return + fi + + echo "[init] + token generated for ${_username}" + echo "${_username}:${_pat}" >> "${TOKENS_FILE}" +} + +# ───────────────────────────────────────────────────────────────────────────── +# 4. Create admin users +# ───────────────────────────────────────────────────────────────────────────── +echo "[init] --- Processing admins ---" +admin_count=$(yq '.admins | length' "${USERS_FILE}") +i=0 +while [ "${i}" -lt "${admin_count}" ]; do + username=$(yq ".admins[${i}].username" "${USERS_FILE}") + email=$(yq ".admins[${i}].email" "${USERS_FILE}") + password=$(yq ".admins[${i}].password" "${USERS_FILE}") + name=$(yq ".admins[${i}].name" "${USERS_FILE}") + + create_user "${username}" "${email}" "${password}" "${name}" '["admin"]' + i=$((i + 1)) +done + +# ───────────────────────────────────────────────────────────────────────────── +# 5. Create regular users +# ───────────────────────────────────────────────────────────────────────────── +echo "[init] --- Processing users ---" +user_count=$(yq '.users | length' "${USERS_FILE}") +i=0 +while [ "${i}" -lt "${user_count}" ]; do + username=$(yq ".users[${i}].username" "${USERS_FILE}") + email=$(yq ".users[${i}].email" "${USERS_FILE}") + password=$(yq ".users[${i}].password" "${USERS_FILE}") + name=$(yq ".users[${i}].name" "${USERS_FILE}") + + create_user "${username}" "${email}" "${password}" "${name}" '["user"]' + i=$((i + 1)) +done + +# ───────────────────────────────────────────────────────────────────────────── +# 6. Generate personal access tokens for ALL users (admins + regular) +# ───────────────────────────────────────────────────────────────────────────── +echo "[init] --- Generating personal access tokens ---" + +# Tokens for admin users +i=0 +while [ "${i}" -lt "${admin_count}" ]; do + username=$(yq ".admins[${i}].username" "${USERS_FILE}") + password=$(yq ".admins[${i}].password" "${USERS_FILE}") + generate_token "${username}" "${password}" + i=$((i + 1)) +done + +# Tokens for regular users +i=0 +while [ "${i}" -lt "${user_count}" ]; do + username=$(yq ".users[${i}].username" "${USERS_FILE}") + password=$(yq ".users[${i}].password" "${USERS_FILE}") + generate_token "${username}" "${password}" + i=$((i + 1)) +done + +# ───────────────────────────────────────────────────────────────────────────── +# 7. Print summary and mark as provisioned +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo "[init] ──────────────────────────────────────" +echo "[init] Provisioning complete." +echo "[init] Tokens written to: ${TOKENS_FILE}" +echo "[init] ──────────────────────────────────────" +if [ -s "${TOKENS_FILE}" ]; then + echo "[init] Token file: /tokens/tokens.txt — use 'make tokens' to retrieve." +fi + +touch "${STAMP_FILE}" +echo "[init] Done." diff --git a/03_container_layer/docker/admin/rocketchat/provisioning/users.yml b/03_container_layer/docker/admin/rocketchat/provisioning/users.yml new file mode 100644 index 0000000..f624711 --- /dev/null +++ b/03_container_layer/docker/admin/rocketchat/provisioning/users.yml @@ -0,0 +1,22 @@ +# !! CHANGE ALL PASSWORDS BEFORE DEPLOYING !! +# Personal access tokens are auto-generated and written to /tokens/tokens.txt + +admins: + - username: rc-admin2 + email: admin2@range42.local + password: "Admin1234!" + name: "RC Admin 2" + +users: + - username: trainee01 + email: trainee01@range42.local + password: "Trainee1234!" + name: "Trainee 01" + - username: trainee02 + email: trainee02@range42.local + password: "Trainee1234!" + name: "Trainee 02" + - username: trainee03 + email: trainee03@range42.local + password: "Trainee1234!" + name: "Trainee 03"