diff --git a/03_container_layer/docker/admin/nextcloud/.dockerignore b/03_container_layer/docker/admin/nextcloud/.dockerignore new file mode 100644 index 0000000..2a768c7 --- /dev/null +++ b/03_container_layer/docker/admin/nextcloud/.dockerignore @@ -0,0 +1,8 @@ +# Prevent secrets and editor artifacts from leaking into the build context. +.env +*.env +*.key +*.pem +*.p12 +*.pfx +README.md diff --git a/03_container_layer/docker/admin/nextcloud/.env.example b/03_container_layer/docker/admin/nextcloud/.env.example new file mode 100644 index 0000000..faa6a5a --- /dev/null +++ b/03_container_layer/docker/admin/nextcloud/.env.example @@ -0,0 +1,21 @@ +# +# ISSUE 146 +# +# Copy to .env and fill in values before running: cp .env.example .env +# .env is gitignored — never commit real secrets. +# + +# ── Nextcloud ────────────────────────────────────────────────────────────── +NC_DOMAIN=localhost + +# ── Initial admin (auto-created by Nextcloud on first boot) ─────────────── +NC_ADMIN_USER=nc-admin +NC_ADMIN_PASS=Admin1234! + +# ── PostgreSQL ────────────────────────────────────────────────────────────── +POSTGRES_USER=nextcloud +POSTGRES_PASSWORD=CHANGEME_replace_before_deploying +POSTGRES_DB=nextcloud + +# ── Host ports ────────────────────────────────────────────────────────────── +HTTP_PORT=8080 diff --git a/03_container_layer/docker/admin/nextcloud/Dockerfile b/03_container_layer/docker/admin/nextcloud/Dockerfile new file mode 100644 index 0000000..ffedee7 --- /dev/null +++ b/03_container_layer/docker/admin/nextcloud/Dockerfile @@ -0,0 +1,32 @@ +# +# ISSUE 146 +# + +# +# BUILDER — php:8.3-apache-bookworm (same base as official Nextcloud Dockerfile) +# Installs yq and jq as static binaries; copies and pre-validates provisioning scripts. +# +FROM php:8.3-apache-bookworm AS builder + +RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates && \ + rm -rf /var/lib/apt/lists/* && \ + curl -sL https://github.com/mikefarah/yq/releases/download/v4.44.1/yq_linux_amd64 \ + -o /usr/local/bin/yq && chmod +x /usr/local/bin/yq && \ + curl -sL https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64 \ + -o /usr/local/bin/jq && chmod +x /usr/local/bin/jq + +COPY provisioning/ /provisioning/ +RUN chmod +x /provisioning/init.sh + +# +# RUNTIME — nextcloud:latest (PHP/Apache/Debian, provisioner via OCS API) +# Copies tooling and provisioning scripts from builder. +# Acts as the provisioner sidecar: creates users and app passwords via OCS API. +# +FROM nextcloud:latest AS runtime + +COPY --from=builder /usr/local/bin/yq /usr/local/bin/yq +COPY --from=builder /usr/local/bin/jq /usr/local/bin/jq +COPY --from=builder /provisioning/ /provisioning/ + +ENTRYPOINT ["/provisioning/init.sh"] diff --git a/03_container_layer/docker/admin/nextcloud/Makefile b/03_container_layer/docker/admin/nextcloud/Makefile new file mode 100644 index 0000000..b2c68c1 --- /dev/null +++ b/03_container_layer/docker/admin/nextcloud/Makefile @@ -0,0 +1,92 @@ +# +# ISSUE 146 +# + +SERVICE = nextcloud +PROVISIONER = nextcloud-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 "" + @echo " Available : " + @echo "" + @echo " make up - run the full stack (db + redis + nextcloud + 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 app passwords" + @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 volume rm -f nextcloud_nextcloud-tokens 2>/dev/null || true + docker compose up -d $(PROVISIONER) + +logs-provisioner: + docker compose logs -f $(PROVISIONER) + +tokens: + docker run --rm -v nextcloud_nextcloud-tokens:/tokens alpine cat /tokens/tokens.txt + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +clean: + docker compose down -v --rmi all + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +term: + docker exec -it $(SERVICE) /bin/bash + +term-debug-build: + docker compose up --build -d debug + docker exec -it $(DEBUG_SERVICE) /bin/bash + +print: help diff --git a/03_container_layer/docker/admin/nextcloud/README.md b/03_container_layer/docker/admin/nextcloud/README.md new file mode 100644 index 0000000..40af5b0 --- /dev/null +++ b/03_container_layer/docker/admin/nextcloud/README.md @@ -0,0 +1,169 @@ +# Nextcloud — Standalone Docker Deployment + +Issue: [#146](https://github.com/range42/range42-catalog/issues/146) + +Standalone Nextcloud instance with automated user provisioning and app-password generation. +The initial admin is created automatically by Nextcloud on first boot; additional users and app +passwords are provisioned via the OCS API sidecar. + +--- + +## 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 +make tokens # print generated app passwords +``` + +Nextcloud will be available at `http://localhost:8080` (or `HTTP_PORT`). +WebDAV: `http://localhost:8080/remote.php/dav/files//` + +--- + +## 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 nextcloud-provisioner registry.example.com/range42/nextcloud-provisioner:latest +docker push registry.example.com/range42/nextcloud-provisioner:latest +``` + +--- + +## Declaring Users + +Edit `provisioning/users.yml` before the first `make up`: + +```yaml +admins: + - username: nc-admin2 + email: admin2@range42.local + password: "Admin1234!" + display_name: "NC Admin 2" + +users: + - username: trainee01 + email: trainee01@range42.local + password: "Trainee1234!" + display_name: "Trainee 01" +``` + +- `admins[]` entries are created and added to the Nextcloud `admin` group. +- `users[]` entries are regular accounts. +- `nc-admin` (set via `NC_ADMIN_USER`) is created automatically by Nextcloud — do not repeat it here. +- An app password is auto-generated for every user and written to `/tokens/tokens.txt`. + +**The provisioner runs only once** (guarded by `/tokens/.provisioned`). +To re-provision after changes, run: + +```bash +make reprovision +``` + +--- + +## App Password Retrieval + +App passwords are written to the `nextcloud-tokens` volume during provisioning. +Retrieve them at any time: + +```bash +make tokens +``` + +Example output: + +``` +nc-admin2: +trainee01: +trainee02: +trainee03: +``` + +--- + +## WebDAV Usage + +Mount a user's files via WebDAV using the generated app password: + +``` +davs://localhost:8080/remote.php/dav/files// +``` + +Example with `curl`: + +```bash +curl -u trainee01: \ + https://localhost:8080/remote.php/dav/files/trainee01/ +``` + +--- + +## API Usage Examples + +```bash +# List files (WebDAV PROPFIND) +curl -X PROPFIND \ + -u trainee01: \ + http://localhost:8080/remote.php/dav/files/trainee01/ + +# Upload a file +curl -T local_file.txt \ + -u trainee01: \ + http://localhost:8080/remote.php/dav/files/trainee01/remote_file.txt + +# OCS user list (admin only) +curl -H "OCS-APIRequest: true" -H "Accept: application/json" \ + -u nc-admin:Admin1234! \ + http://localhost:8080/ocs/v1.php/cloud/users +``` + +--- + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `NC_DOMAIN` | `localhost` | Trusted domain for Nextcloud | +| `NC_ADMIN_USER` | `nc-admin` | Initial admin username (auto-created by Nextcloud) | +| `NC_ADMIN_PASS` | `Admin1234!` | Initial admin password | +| `POSTGRES_USER` | `nextcloud` | DB user | +| `POSTGRES_PASSWORD` | `nextcloud` | DB password — **change before deploying** | +| `POSTGRES_DB` | `nextcloud` | DB name | +| `HTTP_PORT` | `8080` | Host port for HTTP | + +--- + +## Troubleshooting + +**Provisioner exits immediately with "Already provisioned"** +Remove the tokens volume and re-run: `make reprovision` + +**Provisioner fails with "Nextcloud did not become healthy after 180 s"** +Nextcloud first-boot can take several minutes. Increase `start_period` in `compose.yml` +or check `docker logs nextcloud` for errors. + +**User creation returns a 403 or 401** +Verify `NC_ADMIN_USER` and `NC_ADMIN_PASS` in `.env` match the actual admin credentials. + +**App password generation fails (ERROR in tokens.txt)** +The OCS v2 endpoint requires the user to exist and be enabled. +Check `make logs-provisioner` for the exact error. + +**Port 8080 already in use** +Set `HTTP_PORT=8081` (or any free port) in `.env`. diff --git a/03_container_layer/docker/admin/nextcloud/compose.yml b/03_container_layer/docker/admin/nextcloud/compose.yml new file mode 100644 index 0000000..8bac8d0 --- /dev/null +++ b/03_container_layer/docker/admin/nextcloud/compose.yml @@ -0,0 +1,90 @@ +# +# ISSUE 146 +# + +services: + + db: + image: postgres:16-alpine + container_name: nextcloud-db + environment: + POSTGRES_USER: ${POSTGRES_USER:-nextcloud} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-nextcloud} + POSTGRES_DB: ${POSTGRES_DB:-nextcloud} + volumes: + - nextcloud-db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-nextcloud}"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + redis: + image: redis:7-alpine + container_name: nextcloud-redis + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + nextcloud: &main + image: nextcloud:latest + container_name: nextcloud + environment: + NEXTCLOUD_ADMIN_USER: ${NC_ADMIN_USER:-nc-admin} + NEXTCLOUD_ADMIN_PASSWORD: ${NC_ADMIN_PASS:-Admin1234!} + NEXTCLOUD_TRUSTED_DOMAINS: ${NC_DOMAIN:-localhost} + POSTGRES_HOST: db + POSTGRES_DB: ${POSTGRES_DB:-nextcloud} + POSTGRES_USER: ${POSTGRES_USER:-nextcloud} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-nextcloud} + REDIS_HOST: redis + REDIS_HOST_PORT: "6379" + volumes: + - nextcloud-data:/var/www/html + ports: + - "${HTTP_PORT:-8080}:80" + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost/status.php | grep -q '\"installed\":true'"] + interval: 20s + timeout: 10s + retries: 15 + start_period: 60s + restart: unless-stopped +# +# #### #### do not remove. +# +# debug: +# <<: *main +# container_name: nextcloud-debug +# command: ["sleep", "infinity"] + + provisioner: + build: + context: . + dockerfile: Dockerfile + container_name: nextcloud-provisioner + environment: + NC_URL: http://nextcloud + NC_ADMIN_USER: ${NC_ADMIN_USER:-nc-admin} + NC_ADMIN_PASS: ${NC_ADMIN_PASS:-Admin1234!} + USERS_FILE: /provisioning/users.yml + volumes: + - nextcloud-tokens:/tokens + depends_on: + nextcloud: + condition: service_healthy + restart: "no" + +volumes: + nextcloud-db-data: + nextcloud-data: + nextcloud-tokens: diff --git a/03_container_layer/docker/admin/nextcloud/provisioning/init.sh b/03_container_layer/docker/admin/nextcloud/provisioning/init.sh new file mode 100644 index 0000000..3e43ab1 --- /dev/null +++ b/03_container_layer/docker/admin/nextcloud/provisioning/init.sh @@ -0,0 +1,164 @@ +#!/bin/sh +# +# ISSUE 146 +# +# Bootstrap script for the Nextcloud provisioner sidecar. +# Runs once after Nextcloud 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 OCS API and added to the admin group. +# Regular users are created via the OCS API. +# App passwords are generated for every user and written to /tokens/tokens.txt. +# +set -eu + +NC_URL="${NC_URL:-http://nextcloud}" +NC_ADMIN_USER="${NC_ADMIN_USER:-nc-admin}" +NC_ADMIN_PASS="${NC_ADMIN_PASS:-Admin1234!}" +USERS_FILE="${USERS_FILE:-/provisioning/users.yml}" +TOKENS_DIR="/tokens" +TOKENS_FILE="${TOKENS_DIR}/tokens.txt" +PROVISION_STAMP="${TOKENS_DIR}/.provisioned" + +# ── 1. Wait for Nextcloud HTTP (max 180 s) ────────────────────────────────── +echo "[init] Waiting for Nextcloud at ${NC_URL} ..." +attempts=0 +until curl -sf "${NC_URL}/status.php" 2>/dev/null | grep -q '"installed":true'; do + attempts=$((attempts + 1)) + if [ "${attempts}" -ge 60 ]; then + echo "[fatal] Nextcloud did not become healthy after 180 s. Aborting." + exit 1 + fi + sleep 3 +done +echo "[init] Nextcloud is up." + +# ── 2. Idempotency guard ──────────────────────────────────────────────────── +if [ -f "${PROVISION_STAMP}" ]; then + echo "[init] Already provisioned (stamp found at ${PROVISION_STAMP}). Exiting." + exit 0 +fi + +mkdir -p "${TOKENS_DIR}" +: > "${TOKENS_FILE}" + +# ── Helper: create a user via OCS API ────────────────────────────────────── +create_user() { + local username="${1}" + local password="${2}" + local email="${3}" + local display_name="${4}" + + resp=$(curl -sf -X POST "${NC_URL}/ocs/v1.php/cloud/users" \ + -u "${NC_ADMIN_USER}:${NC_ADMIN_PASS}" \ + -H "OCS-APIRequest: true" \ + -H "Accept: application/json" \ + --data-urlencode "userid=${username}" \ + --data-urlencode "password=${password}" \ + --data-urlencode "email=${email}" \ + --data-urlencode "displayName=${display_name}" \ + || echo '{}') + status=$(echo "${resp}" | jq -r '.ocs.meta.statuscode // 999' 2>/dev/null || echo 999) + case "${status}" in + 100) echo "[init] + user created: ${username}" ;; + 102) echo "[warn] ${username} already exists — skipping" ;; + *) echo "[error] Failed to create ${username} (OCS status ${status}): $(echo "${resp}" | jq -r '.ocs.meta.message // "unknown error"' 2>/dev/null)"; exit 1 ;; + esac +} + +# ── Helper: add a user to the admin group ────────────────────────────────── +add_to_admin_group() { + local username="${1}" + + echo "[init] + adding ${username} to admin group" + curl -sf -X POST "${NC_URL}/ocs/v1.php/cloud/groups/admin/users" \ + -u "${NC_ADMIN_USER}:${NC_ADMIN_PASS}" \ + -H "OCS-APIRequest: true" \ + -H "Accept: application/json" \ + --data-urlencode "userid=${username}" \ + >/dev/null \ + || echo "[warn] Failed to add ${username} to admin group" +} + +# ── Helper: generate an app password for a user ──────────────────────────── +generate_app_password() { + local username="${1}" + local password="${2}" + + echo "[init] + generating app password for ${username}" + app_pass_resp=$(curl -sf -X POST "${NC_URL}/ocs/v2.php/core/apppassword" \ + -u "${username}:${password}" \ + -H "OCS-APIRequest: true" \ + -H "Accept: application/json" \ + || echo '{}') + + app_pass=$(printf '%s' "${app_pass_resp}" | jq -r '.ocs.data.apppassword // "ERROR"' 2>/dev/null || echo "ERROR") + + if [ "${app_pass}" = "ERROR" ] || [ -z "${app_pass}" ]; then + echo "[warn] Could not generate app password for ${username}" + app_pass="ERROR" + fi + + printf '%s: %s\n' "${username}" "${app_pass}" >> "${TOKENS_FILE}" + printf '[token] %s: %s\n' "${username}" "${app_pass}" +} + +# ── 3. Admin users ────────────────────────────────────────────────────────── +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}") + display_name=$(yq e ".admins[${i}].display_name // \"\"" "${USERS_FILE}") + + create_user "${username}" "${password}" "${email}" "${display_name}" + add_to_admin_group "${username}" + + i=$((i + 1)) +done + +# ── 4. Regular users ──────────────────────────────────────────────────────── +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}") + display_name=$(yq e ".users[${i}].display_name // \"\"" "${USERS_FILE}") + + create_user "${username}" "${password}" "${email}" "${display_name}" + + i=$((i + 1)) +done + +# ── 4b. Wait for user accounts to be ready before generating app passwords ── +echo "[init] Waiting for user accounts to be ready ..." +sleep 2 + +# ── 4c. Generate app passwords for all users ──────────────────────────────── +echo "[init] Generating app passwords ..." + +i=0 +while [ "${i}" -lt "${admin_count}" ]; do + username=$(yq e ".admins[${i}].username" "${USERS_FILE}") + password=$(yq e ".admins[${i}].password" "${USERS_FILE}") + generate_app_password "${username}" "${password}" + i=$((i + 1)) +done + +i=0 +while [ "${i}" -lt "${user_count}" ]; do + username=$(yq e ".users[${i}].username" "${USERS_FILE}") + password=$(yq e ".users[${i}].password" "${USERS_FILE}") + generate_app_password "${username}" "${password}" + i=$((i + 1)) +done + +# ── 5. Mark as provisioned ────────────────────────────────────────────────── +touch "${PROVISION_STAMP}" +echo "[init] Provisioning complete. App passwords written to ${TOKENS_FILE}." diff --git a/03_container_layer/docker/admin/nextcloud/provisioning/users.yml b/03_container_layer/docker/admin/nextcloud/provisioning/users.yml new file mode 100644 index 0000000..163b50f --- /dev/null +++ b/03_container_layer/docker/admin/nextcloud/provisioning/users.yml @@ -0,0 +1,25 @@ +# !! CHANGE ALL PASSWORDS BEFORE DEPLOYING !! +# App passwords are auto-generated by the provisioner and written to /tokens/tokens.txt +# +# Note: nc-admin (NEXTCLOUD_ADMIN_USER) is created automatically by Nextcloud on first boot. +# This file declares ADDITIONAL users to be provisioned via the OCS API. + +admins: + - username: nc-admin2 + email: admin2@range42.local + password: "Admin1234!" + display_name: "NC Admin 2" + +users: + - username: trainee01 + email: trainee01@range42.local + password: "Trainee1234!" + display_name: "Trainee 01" + - username: trainee02 + email: trainee02@range42.local + password: "Trainee1234!" + display_name: "Trainee 02" + - username: trainee03 + email: trainee03@range42.local + password: "Trainee1234!" + display_name: "Trainee 03"