From 1793eda07f17a3b13917ec33ba1d5ad6b9abde9c Mon Sep 17 00:00:00 2001 From: t0kubetsu Date: Mon, 11 May 2026 15:43:19 +0200 Subject: [PATCH 1/3] feat: add Mattermost Docker deployment with user provisioning (#143) - Two-stage Dockerfile: golang:alpine builder + mattermost/mattermost-team-edition:latest runtime - compose.yml: db (postgres:16-alpine), mattermost, provisioner services with healthchecks - provisioning/init.sh: CLI user creation + REST API for team, membership, and PAT generation - provisioning/users.yml: declarative user manifest (admins + trainees) - Makefile: build/up/down/logs/tokens/reprovision targets matching gitea pattern - .env.example, .dockerignore, README.md included --- .../docker/admin/mattermost/.dockerignore | 6 + .../docker/admin/mattermost/.env.example | 24 +++ .../docker/admin/mattermost/Dockerfile | 30 +++ .../docker/admin/mattermost/Makefile | 95 +++++++++ .../docker/admin/mattermost/README.md | 150 ++++++++++++++ .../docker/admin/mattermost/compose.yml | 90 +++++++++ .../admin/mattermost/provisioning/init.sh | 185 ++++++++++++++++++ .../admin/mattermost/provisioning/users.yml | 24 +++ 8 files changed, 604 insertions(+) create mode 100644 03_container_layer/docker/admin/mattermost/.dockerignore create mode 100644 03_container_layer/docker/admin/mattermost/.env.example create mode 100644 03_container_layer/docker/admin/mattermost/Dockerfile create mode 100644 03_container_layer/docker/admin/mattermost/Makefile create mode 100644 03_container_layer/docker/admin/mattermost/README.md create mode 100644 03_container_layer/docker/admin/mattermost/compose.yml create mode 100644 03_container_layer/docker/admin/mattermost/provisioning/init.sh create mode 100644 03_container_layer/docker/admin/mattermost/provisioning/users.yml diff --git a/03_container_layer/docker/admin/mattermost/.dockerignore b/03_container_layer/docker/admin/mattermost/.dockerignore new file mode 100644 index 0000000..2ab26d7 --- /dev/null +++ b/03_container_layer/docker/admin/mattermost/.dockerignore @@ -0,0 +1,6 @@ +# Prevent secrets and editor artifacts from leaking into the build context. +.env +*.env +*.key +*.pem +README.md diff --git a/03_container_layer/docker/admin/mattermost/.env.example b/03_container_layer/docker/admin/mattermost/.env.example new file mode 100644 index 0000000..34488f3 --- /dev/null +++ b/03_container_layer/docker/admin/mattermost/.env.example @@ -0,0 +1,24 @@ +# +# ISSUE 143 +# +# Copy to .env and fill in values before running: cp .env.example .env +# .env is gitignored — never commit real secrets. +# + +# ── Mattermost ───────────────────────────────────────────────────────────── +MM_BASE_URL=http://localhost:8065 + +# ── Initial admin (must match admins[0] in provisioning/users.yml) ───────── +MM_ADMIN_USER=mm-admin +MM_ADMIN_PASS=Admin1234! + +# ── Default team ──────────────────────────────────────────────────────────── +MM_TEAM_NAME=range42 + +# ── PostgreSQL ────────────────────────────────────────────────────────────── +POSTGRES_USER=mattermost +POSTGRES_PASSWORD=CHANGEME_replace_before_deploying +POSTGRES_DB=mattermost + +# ── Host ports ────────────────────────────────────────────────────────────── +HTTP_PORT=8065 diff --git a/03_container_layer/docker/admin/mattermost/Dockerfile b/03_container_layer/docker/admin/mattermost/Dockerfile new file mode 100644 index 0000000..19bba70 --- /dev/null +++ b/03_container_layer/docker/admin/mattermost/Dockerfile @@ -0,0 +1,30 @@ +# +# ISSUE 143 +# + +# +# BUILDER — golang:alpine (Mattermost is Go-based) +# Installs yq for YAML parsing; copies and pre-validates provisioning scripts. +# +FROM golang:alpine AS builder + +RUN apk add --no-cache yq jq + +COPY provisioning/ /provisioning/ +RUN chmod +x /provisioning/init.sh + +# +# RUNTIME — mattermost/mattermost-team-edition:latest (Alpine-based, ships mattermost binary) +# Copies tooling and provisioning scripts from builder. +# Acts as the provisioner sidecar: creates users via CLI + REST API for tokens. +# +FROM mattermost/mattermost-team-edition:latest AS runtime + +COPY --from=builder /usr/bin/yq /usr/bin/yq +COPY --from=builder /usr/bin/jq /usr/bin/jq +COPY --from=builder /provisioning/ /provisioning/ + +USER root +RUN chmod +x /provisioning/init.sh + +ENTRYPOINT ["/provisioning/init.sh"] diff --git a/03_container_layer/docker/admin/mattermost/Makefile b/03_container_layer/docker/admin/mattermost/Makefile new file mode 100644 index 0000000..60217ff --- /dev/null +++ b/03_container_layer/docker/admin/mattermost/Makefile @@ -0,0 +1,95 @@ +# +# ISSUE 143 +# + +SERVICE = mattermost +PROVISIONER = mattermost-provisioner +DEBUG_SERVICE = $(SERVICE)-debug + +help: + @echo "" + @echo "" + @echo " Available : " + @echo "" + @echo " make up - run the full stack (db + mattermost + provisioner) in background" + @echo " make down - stop and remove all containers" + @echo " make stop - stop $(SERVICE) only" + @echo "" + @echo " make build - build the provisioner image" + @echo " make build-up - build provisioner image then start the full stack" + @echo "" + @echo " make rebuild - full rebuild without cache" + @echo " make rebuild-up - full rebuild without cache then start" + @echo "" + @echo " make reprovision - remove the provisioning stamp and restart provisioner" + @echo " make logs-provisioner - tail provisioner output" + @echo " make tokens - print generated personal access tokens" + @echo "" + @echo " make term - open shell in $(SERVICE) container" + @echo " make term-debug-build - build and open shell in $(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 +down: + docker compose down +stop: + docker compose stop $(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 + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +reprovision: + docker exec $(SERVICE) rm -f /tokens/.provisioned + docker compose restart $(PROVISIONER) + +logs-provisioner: + docker compose logs -f $(PROVISIONER) + +tokens: + docker exec $(PROVISIONER) cat /tokens/tokens.txt + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +clean: + docker system prune -a --volumes -f + -docker stop $(shell docker ps -aq) + -docker rm -f $(shell docker ps -aq) + -docker rmi -f $(shell docker images -q) + -docker volume rm -f $(shell docker volume ls -q) + -docker network prune -f + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +term: + docker exec -it $(SERVICE) /bin/sh + +term-debug-build: + docker compose up --build -d debug + docker exec -it $(DEBUG_SERVICE) /bin/sh + +print: help diff --git a/03_container_layer/docker/admin/mattermost/README.md b/03_container_layer/docker/admin/mattermost/README.md new file mode 100644 index 0000000..5209162 --- /dev/null +++ b/03_container_layer/docker/admin/mattermost/README.md @@ -0,0 +1,150 @@ +# Mattermost — Standalone Docker Deployment + +Issue: [#143](https://github.com/range42/range42-catalog/issues/143) + +Standalone Mattermost Team Edition instance with automated user provisioning +and personal access token generation. +All accounts are declared in `provisioning/users.yml`; registration is disabled +via environment variables by default. + +--- + +## Prerequisites + +- Docker 24+ with Compose v2 +- `make` + +--- + +## Quick Start + +```bash +cp .env.example .env # edit secrets before deploying +make build-up # build provisioner image, start full stack +make logs-provisioner # watch bootstrap output +``` + +Mattermost will be available at `http://localhost:8065` (or `MM_BASE_URL`). + +--- + +## Build & Push + +```bash +# Build only the provisioner image +make build + +# Full rebuild (no cache) +make rebuild + +# Push to a registry (replace tag as needed) +docker tag mattermost-provisioner registry.example.com/range42/mattermost-provisioner:latest +docker push registry.example.com/range42/mattermost-provisioner:latest +``` + +--- + +## Declaring Users + +Edit `provisioning/users.yml` before the first `make up`: + +```yaml +admins: + - username: mm-admin + email: admin@range42.local + password: "Admin1234!" + +users: + - username: trainee01 + email: trainee01@range42.local + password: "Trainee1234!" +``` + +- Add/remove entries to change the provisioned user set. +- `admins[]` entries receive Mattermost system-admin privileges. +- `users[]` entries are regular accounts. + +**The provisioner runs only once** (guarded by `/tokens/.provisioned`). +To re-provision after changes, run: + +```bash +make reprovision +``` + +--- + +## Token Retrieval + +Personal access tokens are generated for every user at provisioning time +and written to `/tokens/tokens.txt` (one `username:token` per line). + +```bash +# Via make target +make tokens + +# Via docker exec +docker exec mattermost-provisioner cat /tokens/tokens.txt +``` + +--- + +## API Usage Examples + +```bash +# Replace with a value from tokens.txt + +# Get current user info +curl -H "Authorization: Bearer " http://localhost:8065/api/v4/users/me + +# List channels in the default team +curl -H "Authorization: Bearer " \ + "http://localhost:8065/api/v4/users/me/teams/channels" + +# Post a message to a channel +curl -X POST http://localhost:8065/api/v4/posts \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"channel_id":"","message":"Hello from the API"}' +``` + +--- + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `MM_BASE_URL` | `http://localhost:8065` | Public URL of the Mattermost instance | +| `MM_ADMIN_USER` | `mm-admin` | Must match `admins[0].username` in `users.yml` | +| `MM_ADMIN_PASS` | `Admin1234!` | Must match `admins[0].password` in `users.yml` | +| `MM_TEAM_NAME` | `range42` | Default team created by provisioner | +| `POSTGRES_USER` | `mattermost` | DB user | +| `POSTGRES_PASSWORD` | *(required)* | DB password | +| `POSTGRES_DB` | `mattermost` | DB name | +| `HTTP_PORT` | `8065` | Host port for Mattermost HTTP | + +--- + +## Troubleshooting + +**Provisioner exits immediately with "Already provisioned"** +Remove the stamp and re-run: `make reprovision` + +**`mattermost user create` fails silently** +Check provisioner logs: `make logs-provisioner` +The stamp is NOT written on failure — restart the provisioner to retry. + +**Token creation returns empty** +Ensure `MM_SERVICESETTINGS_ENABLEUSERACCESSTOKENS=true` is set (already the +default in `compose.yml`). Verify with: +```bash +curl http://localhost:8065/api/v4/config/client?format=old | jq '.EnableUserAccessTokens' +``` + +**Port 8065 already in use** +Set `HTTP_PORT=8066` (or any free port) in `.env`. + +**Mattermost fails to start / DB connection refused** +Check that the `db` service passed its healthcheck before `mattermost` started: +```bash +docker compose logs db +``` diff --git a/03_container_layer/docker/admin/mattermost/compose.yml b/03_container_layer/docker/admin/mattermost/compose.yml new file mode 100644 index 0000000..18b107f --- /dev/null +++ b/03_container_layer/docker/admin/mattermost/compose.yml @@ -0,0 +1,90 @@ +# +# ISSUE 143 +# + +services: + + db: + image: postgres:16-alpine + container_name: mattermost-db + environment: + POSTGRES_USER: ${POSTGRES_USER:-mattermost} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-mattermost} + POSTGRES_DB: ${POSTGRES_DB:-mattermost} + volumes: + - mattermost-db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-mattermost}"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + mattermost: &main + image: mattermost/mattermost-team-edition:latest + container_name: mattermost + environment: + MM_SQLSETTINGS_DRIVERNAME: postgres + MM_SQLSETTINGS_DATASOURCE: "postgres://${POSTGRES_USER:-mattermost}:${POSTGRES_PASSWORD:-mattermost}@db:5432/${POSTGRES_DB:-mattermost}?sslmode=disable&connect_timeout=10" + MM_SERVICESETTINGS_SITEURL: ${MM_BASE_URL:-http://localhost:8065} + MM_SERVICESETTINGS_ENABLEUSERSTATUSESFORPOSTS: "false" + MM_SERVICESETTINGS_ENABLEAPITEAMCREATION: "true" + MM_SERVICESETTINGS_ENABLEAPICREATEACCOUNT: "true" + MM_SERVICESETTINGS_ENABLEUSERACCESSTOKENS: "true" + MM_TEAMSETTINGS_MAXUSERSPERTEAM: "200" + MM_TEAMSETTINGS_ENABLEOPENSERVER: "true" + MM_EMAILSETTINGS_SENDEMAILNOTIFICATIONS: "false" + MM_EMAILSETTINGS_REQUIREEMAILVERIFICATION: "false" + MM_LOGSETTINGS_CONSOLELEVEL: "ERROR" + MM_LOGSETTINGS_ENABLEFILE: "false" + volumes: + - mattermost-data:/mattermost/data + - mattermost-logs:/mattermost/logs + - mattermost-config:/mattermost/config + - mattermost-plugins:/mattermost/plugins + ports: + - "${HTTP_PORT:-8065}:8065" + depends_on: + db: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8065/api/v4/system/ping"] + interval: 15s + timeout: 10s + retries: 15 + start_period: 60s + restart: unless-stopped +# +# #### #### do not remove. +# +# debug: +# <<: *main +# container_name: mattermost-debug +# command: ["sleep", "infinity"] + + provisioner: + build: + context: . + dockerfile: Dockerfile + container_name: mattermost-provisioner + environment: + MM_URL: http://mattermost:8065 + MM_ADMIN_USER: ${MM_ADMIN_USER:-mm-admin} + MM_ADMIN_PASS: ${MM_ADMIN_PASS:-Admin1234!} + MM_TEAM_NAME: ${MM_TEAM_NAME:-range42} + USERS_FILE: /provisioning/users.yml + volumes: + - mattermost-config:/mattermost/config + - mattermost-tokens:/tokens + depends_on: + mattermost: + condition: service_healthy + restart: "no" + +volumes: + mattermost-db-data: + mattermost-data: + mattermost-logs: + mattermost-config: + mattermost-plugins: + mattermost-tokens: diff --git a/03_container_layer/docker/admin/mattermost/provisioning/init.sh b/03_container_layer/docker/admin/mattermost/provisioning/init.sh new file mode 100644 index 0000000..743a3df --- /dev/null +++ b/03_container_layer/docker/admin/mattermost/provisioning/init.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env sh +# +# ISSUE 143 +# +# Bootstrap script for the Mattermost provisioner sidecar. +# Runs once after Mattermost is healthy; guarded by a stamp file for idempotency. +# +# User declarations come from USERS_FILE (default: /provisioning/users.yml). +# Admin users are created via the mattermost CLI (direct DB access via config.json). +# Personal access tokens are generated via the Mattermost REST API. +# Tokens are written to /tokens/tokens.txt and to stdout. +# +set -eu + +MM_URL="${MM_URL:-http://mattermost:8065}" +MM_ADMIN_USER="${MM_ADMIN_USER:-mm-admin}" +MM_ADMIN_PASS="${MM_ADMIN_PASS:-Admin1234!}" +MM_TEAM_NAME="${MM_TEAM_NAME:-range42}" +USERS_FILE="${USERS_FILE:-/provisioning/users.yml}" +MM_CONFIG="/mattermost/config/config.json" +PROVISION_STAMP="/tokens/.provisioned" +TOKENS_FILE="/tokens/tokens.txt" + +# ── 1. Wait for Mattermost HTTP (max 180 s) ───────────────────────────────── +echo "[init] Waiting for Mattermost at ${MM_URL} ..." +attempts=0 +until curl -sf "${MM_URL}/api/v4/system/ping" >/dev/null 2>&1; do + attempts=$((attempts + 1)) + if [ "${attempts}" -ge 60 ]; then + echo "[fatal] Mattermost did not become healthy after 180 s. Aborting." + exit 1 + fi + sleep 3 +done +echo "[init] Mattermost is up." + +# ── 2. Idempotency guard ───────────────────────────────────────────────────── +if [ -f "${PROVISION_STAMP}" ]; then + echo "[init] Already provisioned (stamp found at ${PROVISION_STAMP}). Exiting." + exit 0 +fi + +# ── 3. Admin users (mattermost CLI — direct DB, no HTTP auth needed) ───────── +admin_count=$(yq e '.admins | length' "${USERS_FILE}") +echo "[init] Creating ${admin_count} admin user(s) ..." + +i=0 +while [ "${i}" -lt "${admin_count}" ]; do + username=$(yq e ".admins[${i}].username" "${USERS_FILE}") + email=$(yq e ".admins[${i}].email" "${USERS_FILE}") + password=$(yq e ".admins[${i}].password" "${USERS_FILE}") + + echo "[init] + admin: ${username}" + mattermost --config "${MM_CONFIG}" user create \ + --email "${email}" \ + --username "${username}" \ + --password "${password}" \ + --system_admin 2>/dev/null \ + || echo "[warn] ${username} may already exist — skipping" + + i=$((i + 1)) +done + +# ── 4. Regular users (mattermost CLI) ──────────────────────────────────────── +user_count=$(yq e '.users | length' "${USERS_FILE}") +echo "[init] Creating ${user_count} regular user(s) ..." + +i=0 +while [ "${i}" -lt "${user_count}" ]; do + username=$(yq e ".users[${i}].username" "${USERS_FILE}") + email=$(yq e ".users[${i}].email" "${USERS_FILE}") + password=$(yq e ".users[${i}].password" "${USERS_FILE}") + + echo "[init] + user: ${username}" + mattermost --config "${MM_CONFIG}" user create \ + --email "${email}" \ + --username "${username}" \ + --password "${password}" 2>/dev/null \ + || echo "[warn] ${username} may already exist — skipping" + + i=$((i + 1)) +done + +# ── 5. Login as first admin via REST API ───────────────────────────────────── +echo "[init] Logging in as ${MM_ADMIN_USER} via REST API ..." +sleep 3 + +auth_response=$(curl -sf -D - -X POST "${MM_URL}/api/v4/users/login" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg u "${MM_ADMIN_USER}" --arg p "${MM_ADMIN_PASS}" \ + '{"login_id":$u,"password":$p}')") + +admin_token=$(printf '%s' "${auth_response}" | grep -i '^Token:' | awk '{print $2}' | tr -d '\r\n') +admin_user=$(printf '%s' "${auth_response}" | tail -1) +admin_id=$(printf '%s' "${admin_user}" | jq -r '.id') + +if [ -z "${admin_token}" ] || [ "${admin_id}" = "null" ]; then + echo "[fatal] Could not obtain admin session token. Aborting." + exit 1 +fi +echo "[init] Admin session established (id=${admin_id})." + +# ── 6. Create default team ──────────────────────────────────────────────────── +echo "[init] Creating team '${MM_TEAM_NAME}' ..." +team_resp=$(curl -sf -X POST "${MM_URL}/api/v4/teams" \ + -H "Authorization: Bearer ${admin_token}" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg n "${MM_TEAM_NAME}" --arg dn "Range42" \ + '{"name":$n,"display_name":$dn,"type":"O"}')") \ + || true +team_id=$(printf '%s' "${team_resp}" | jq -r '.id // empty') +if [ -z "${team_id}" ]; then + # Team may already exist; look it up + team_id=$(curl -sf "${MM_URL}/api/v4/teams/name/${MM_TEAM_NAME}" \ + -H "Authorization: Bearer ${admin_token}" | jq -r '.id') +fi +echo "[init] Team id=${team_id}." + +# ── 7. Add all users to team ────────────────────────────────────────────────── +add_to_team() { + local section="${1}" + local count j uname uid + + count=$(yq e ".${section} | length" "${USERS_FILE}") + j=0 + while [ "${j}" -lt "${count}" ]; do + uname=$(yq e ".${section}[${j}].username" "${USERS_FILE}") + uid=$(curl -sf "${MM_URL}/api/v4/users/username/${uname}" \ + -H "Authorization: Bearer ${admin_token}" | jq -r '.id') + + echo "[init] + team member: ${uname} (${uid})" + curl -sf -X POST "${MM_URL}/api/v4/teams/${team_id}/members" \ + -H "Authorization: Bearer ${admin_token}" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg uid "${uid}" --arg tid "${team_id}" \ + '{"team_id":$tid,"user_id":$uid}')" \ + >/dev/null \ + || echo "[warn] Could not add ${uname} to team — may already be a member" + + j=$((j + 1)) + done +} + +echo "[init] Adding users to team '${MM_TEAM_NAME}' ..." +add_to_team admins +add_to_team users + +# ── 8. Generate personal access tokens for all users ───────────────────────── +echo "[init] Generating personal access tokens ..." +: > "${TOKENS_FILE}" + +generate_tokens() { + local section="${1}" + local count j uname uid token_val + + count=$(yq e ".${section} | length" "${USERS_FILE}") + j=0 + while [ "${j}" -lt "${count}" ]; do + uname=$(yq e ".${section}[${j}].username" "${USERS_FILE}") + uid=$(curl -sf "${MM_URL}/api/v4/users/username/${uname}" \ + -H "Authorization: Bearer ${admin_token}" | jq -r '.id') + + token_resp=$(curl -sf -X POST "${MM_URL}/api/v4/users/${uid}/tokens" \ + -H "Authorization: Bearer ${admin_token}" \ + -H "Content-Type: application/json" \ + -d "$(jq -n '{"description":"API access token"}')") + token_val=$(printf '%s' "${token_resp}" | jq -r '.token // empty') + + if [ -n "${token_val}" ]; then + printf '%s:%s\n' "${uname}" "${token_val}" | tee -a "${TOKENS_FILE}" + echo "[init] + token for ${uname}: OK" + else + echo "[warn] Could not create token for ${uname}" + fi + + j=$((j + 1)) + done +} + +generate_tokens admins +generate_tokens users + +# ── 9. Mark as provisioned ──────────────────────────────────────────────────── +touch "${PROVISION_STAMP}" +echo "[init] Provisioning complete. Tokens written to ${TOKENS_FILE}." diff --git a/03_container_layer/docker/admin/mattermost/provisioning/users.yml b/03_container_layer/docker/admin/mattermost/provisioning/users.yml new file mode 100644 index 0000000..8327774 --- /dev/null +++ b/03_container_layer/docker/admin/mattermost/provisioning/users.yml @@ -0,0 +1,24 @@ +# Declarative user provisioning for the Mattermost instance. +# Processed by provisioning/init.sh on first container start. +# +# !! CHANGE ALL PASSWORDS BEFORE DEPLOYING !! +# Passwords here are committed in plaintext. Use strong, unique values in production. +# +# To scale: add/remove entries under admins: or users: +# The provisioner is idempotent — re-running skips already-created accounts. + +admins: + - username: mm-admin + email: admin@range42.local + password: "Admin1234!" + +users: + - username: trainee01 + email: trainee01@range42.local + password: "Trainee1234!" + - username: trainee02 + email: trainee02@range42.local + password: "Trainee1234!" + - username: trainee03 + email: trainee03@range42.local + password: "Trainee1234!" From d4c7f14d63ddef47eda2735b97a2c1b35eb432a2 Mon Sep 17 00:00:00 2001 From: t0kubetsu Date: Mon, 11 May 2026 16:12:19 +0200 Subject: [PATCH 2/3] fix: address critical review findings in mattermost provisioner (#151) - Fix reprovision: remove stamp via docker run against named volume, not docker exec on wrong container - Fix make tokens: use docker run against named volume (docker exec fails on stopped container) - Fix token leak: write tokens to file only, not stdout/logs - Fix yq: explicitly install mikefarah/yq v4.44.1 - Add --email-verified to mattermost user create - Fix make clean: scope to project only - Replace fixed sleep with retry loop for REST login --- .../docker/admin/mattermost/Dockerfile | 4 +- .../docker/admin/mattermost/Makefile | 13 ++----- .../admin/mattermost/provisioning/init.sh | 39 ++++++++++++------- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/03_container_layer/docker/admin/mattermost/Dockerfile b/03_container_layer/docker/admin/mattermost/Dockerfile index 19bba70..7ba1352 100644 --- a/03_container_layer/docker/admin/mattermost/Dockerfile +++ b/03_container_layer/docker/admin/mattermost/Dockerfile @@ -8,7 +8,9 @@ # FROM golang:alpine AS builder -RUN apk add --no-cache yq jq +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 diff --git a/03_container_layer/docker/admin/mattermost/Makefile b/03_container_layer/docker/admin/mattermost/Makefile index 60217ff..3a23f26 100644 --- a/03_container_layer/docker/admin/mattermost/Makefile +++ b/03_container_layer/docker/admin/mattermost/Makefile @@ -64,24 +64,19 @@ rebuild-up: # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # reprovision: - docker exec $(SERVICE) rm -f /tokens/.provisioned - docker compose restart $(PROVISIONER) + docker run --rm -v mattermost-tokens:/tokens alpine rm -f /tokens/.provisioned + docker compose up $(PROVISIONER) logs-provisioner: docker compose logs -f $(PROVISIONER) tokens: - docker exec $(PROVISIONER) cat /tokens/tokens.txt + docker run --rm -v mattermost-tokens:/tokens alpine cat /tokens/tokens.txt # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # clean: - docker system prune -a --volumes -f - -docker stop $(shell docker ps -aq) - -docker rm -f $(shell docker ps -aq) - -docker rmi -f $(shell docker images -q) - -docker volume rm -f $(shell docker volume ls -q) - -docker network prune -f + docker compose down -v --rmi all # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # diff --git a/03_container_layer/docker/admin/mattermost/provisioning/init.sh b/03_container_layer/docker/admin/mattermost/provisioning/init.sh index 743a3df..93d1851 100644 --- a/03_container_layer/docker/admin/mattermost/provisioning/init.sh +++ b/03_container_layer/docker/admin/mattermost/provisioning/init.sh @@ -52,10 +52,11 @@ while [ "${i}" -lt "${admin_count}" ]; do echo "[init] + admin: ${username}" mattermost --config "${MM_CONFIG}" user create \ - --email "${email}" \ + --email "${email}" \ --username "${username}" \ --password "${password}" \ - --system_admin 2>/dev/null \ + --system_admin \ + --email-verified 2>/dev/null \ || echo "[warn] ${username} may already exist — skipping" i=$((i + 1)) @@ -73,24 +74,34 @@ while [ "${i}" -lt "${user_count}" ]; do echo "[init] + user: ${username}" mattermost --config "${MM_CONFIG}" user create \ - --email "${email}" \ + --email "${email}" \ --username "${username}" \ - --password "${password}" 2>/dev/null \ + --password "${password}" \ + --email-verified 2>/dev/null \ || echo "[warn] ${username} may already exist — skipping" i=$((i + 1)) done # ── 5. Login as first admin via REST API ───────────────────────────────────── -echo "[init] Logging in as ${MM_ADMIN_USER} via REST API ..." -sleep 3 - -auth_response=$(curl -sf -D - -X POST "${MM_URL}/api/v4/users/login" \ - -H "Content-Type: application/json" \ - -d "$(jq -n --arg u "${MM_ADMIN_USER}" --arg p "${MM_ADMIN_PASS}" \ - '{"login_id":$u,"password":$p}')") +echo "[init] Waiting for REST API to recognize provisioned users ..." +mm_token="" +attempts=0 +until [ -n "${mm_token}" ] && [ "${mm_token}" != "null" ]; do + attempts=$((attempts + 1)) + if [ "${attempts}" -ge 20 ]; then + echo "[fatal] Could not log in as admin after 60 s" + exit 1 + fi + auth_resp=$(curl -sf -D - -X POST "${MM_URL}/api/v4/users/login" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg u "${MM_ADMIN_USER}" --arg p "${MM_ADMIN_PASS}" '{"login_id":$u,"password":$p}')" 2>/dev/null || echo "") + mm_token=$(echo "${auth_resp}" | grep -i '^Token:' | awk '{print $2}' | tr -d '\r') + [ -z "${mm_token}" ] && sleep 3 +done -admin_token=$(printf '%s' "${auth_response}" | grep -i '^Token:' | awk '{print $2}' | tr -d '\r\n') +auth_response="${auth_resp}" +admin_token="${mm_token}" admin_user=$(printf '%s' "${auth_response}" | tail -1) admin_id=$(printf '%s' "${admin_user}" | jq -r '.id') @@ -167,8 +178,8 @@ generate_tokens() { token_val=$(printf '%s' "${token_resp}" | jq -r '.token // empty') if [ -n "${token_val}" ]; then - printf '%s:%s\n' "${uname}" "${token_val}" | tee -a "${TOKENS_FILE}" - echo "[init] + token for ${uname}: OK" + printf '%s:%s\n' "${uname}" "${token_val}" >> "${TOKENS_FILE}" + echo "[init] + token generated for ${uname}" else echo "[warn] Could not create token for ${uname}" fi From d3580a0a4d13df1b65d50a9e12bb0e0bd9948d8a Mon Sep 17 00:00:00 2001 From: t0kubetsu Date: Mon, 11 May 2026 16:52:20 +0200 Subject: [PATCH 3/3] fix: add .PHONY declarations to Makefile --- 03_container_layer/docker/admin/mattermost/Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/03_container_layer/docker/admin/mattermost/Makefile b/03_container_layer/docker/admin/mattermost/Makefile index 3a23f26..254893e 100644 --- a/03_container_layer/docker/admin/mattermost/Makefile +++ b/03_container_layer/docker/admin/mattermost/Makefile @@ -6,6 +6,8 @@ SERVICE = mattermost PROVISIONER = mattermost-provisioner DEBUG_SERVICE = $(SERVICE)-debug +.PHONY: help up down stop build rebuild build-up rebuild-up reprovision logs-provisioner tokens term term-debug-build clean print + help: @echo "" @echo ""