diff --git a/03_container_layer/docker/admin/gitea/.dockerignore b/03_container_layer/docker/admin/gitea/.dockerignore new file mode 100644 index 0000000..2a768c7 --- /dev/null +++ b/03_container_layer/docker/admin/gitea/.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/gitea/.env.example b/03_container_layer/docker/admin/gitea/.env.example new file mode 100644 index 0000000..15b9d9f --- /dev/null +++ b/03_container_layer/docker/admin/gitea/.env.example @@ -0,0 +1,28 @@ +# +# ISSUE 141 +# +# Copy to .env and fill in values before running: cp .env.example .env +# .env is gitignored — never commit real secrets. +# + +# ── Gitea ────────────────────────────────────────────────────────────────── +GITEA_DOMAIN=localhost +GITEA_BASE_URL=http://localhost:3000 + +# Generate with: openssl rand -hex 32 +GITEA_SECRET_KEY=please-change-me-in-production +# Generate with: gitea generate secret INTERNAL_TOKEN +GITEA_INTERNAL_TOKEN=please-change-me-in-production + +# ── Initial admin (must match admins[0] in provisioning/users.yml) ───────── +GITEA_ADMIN_USER=gitea-admin +GITEA_ADMIN_PASS=Admin1234! + +# ── PostgreSQL ────────────────────────────────────────────────────────────── +POSTGRES_USER=gitea +POSTGRES_PASSWORD=CHANGEME_replace_before_deploying +POSTGRES_DB=gitea + +# ── Host ports ────────────────────────────────────────────────────────────── +HTTP_PORT=3000 +SSH_PORT=2222 diff --git a/03_container_layer/docker/admin/gitea/Dockerfile b/03_container_layer/docker/admin/gitea/Dockerfile new file mode 100644 index 0000000..a145c5c --- /dev/null +++ b/03_container_layer/docker/admin/gitea/Dockerfile @@ -0,0 +1,29 @@ +# +# ISSUE 141 +# + +# +# BUILDER — golang:alpine (same base as official Gitea Dockerfile) +# Installs yq for YAML parsing; copies and pre-validates provisioning scripts. +# +FROM golang: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 — gitea/gitea:latest (Alpine-based, ships the gitea CLI) +# Copies tooling and provisioning scripts from builder. +# Acts as the provisioner sidecar: creates users via CLI + SSH keys via REST API. +# +FROM gitea/gitea: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/ + +ENTRYPOINT ["/provisioning/init.sh"] diff --git a/03_container_layer/docker/admin/gitea/Makefile b/03_container_layer/docker/admin/gitea/Makefile new file mode 100644 index 0000000..0cf9e16 --- /dev/null +++ b/03_container_layer/docker/admin/gitea/Makefile @@ -0,0 +1,89 @@ +# +# ISSUE 141 +# + +SERVICE = gitea +PROVISIONER = gitea-provisioner +DEBUG_SERVICE = $(SERVICE)-debug + +.PHONY: up down stop build rebuild build-up rebuild-up reprovision \ + logs-provisioner term term-debug-build clean print help + +help: + @echo "" + @echo "" + @echo " Available : " + @echo "" + @echo " make up - run the full stack (db + gitea + 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 "" + @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 /data/gitea/.provisioned + docker compose restart $(PROVISIONER) + +logs-provisioner: + docker compose logs -f $(PROVISIONER) + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +clean: + docker compose down -v --rmi all + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +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/gitea/README.md b/03_container_layer/docker/admin/gitea/README.md new file mode 100644 index 0000000..1b959db --- /dev/null +++ b/03_container_layer/docker/admin/gitea/README.md @@ -0,0 +1,129 @@ +# Gitea — Standalone Docker Deployment + +Issue: [#141](https://github.com/range42/range42-catalog/issues/141) + +Standalone Gitea instance with automated user and SSH-key provisioning. +Registration is disabled by default; all accounts are declared in `provisioning/users.yml`. + +--- + +## 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 +``` + +Gitea will be available at `http://localhost:3000` (or `GITEA_BASE_URL`). +SSH cloning: `git clone git@localhost:2222//.git` + +--- + +## 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 gitea-provisioner registry.example.com/range42/gitea-provisioner:latest +docker push registry.example.com/range42/gitea-provisioner:latest +``` + +--- + +## Declaring Users and SSH Keys + +Edit `provisioning/users.yml` before the first `make up`: + +```yaml +admins: + - username: gitea-admin + email: admin@range42.local + password: "Admin1234!" + ssh_keys: + - "ssh-ed25519 AAAA... user@host" # full public key string + +users: + - username: trainee01 + email: trainee01@range42.local + password: "Trainee1234!" + ssh_keys: [] # no SSH key for this user +``` + +- Add/remove entries to change the provisioned user set. +- `admins[]` entries receive Gitea admin privileges. +- `users[]` entries are regular accounts. +- `ssh_keys` is a list of raw public-key strings (same format as `~/.ssh/authorized_keys`). + +**The provisioner runs only once** (guarded by `/data/gitea/.provisioned`). +To re-provision after changes, run: + +```bash +make reprovision +``` + +--- + +## SSH Key Format + +Accepted algorithms: `ssh-ed25519`, `ssh-rsa`, `ecdsa-sha2-nistp256/384/521`. + +``` +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... comment +``` + +Generate a new key pair: + +```bash +ssh-keygen -t ed25519 -C "trainee01@range42" -f ~/.ssh/range42_trainee01 +``` + +Paste the contents of `~/.ssh/range42_trainee01.pub` into the `ssh_keys` list. + +--- + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `GITEA_DOMAIN` | `localhost` | Public hostname | +| `GITEA_BASE_URL` | `http://localhost:3000` | Root URL shown in clone URLs | +| `GITEA_SECRET_KEY` | *(required)* | App secret — `openssl rand -hex 32` | +| `GITEA_INTERNAL_TOKEN` | *(required)* | Internal token — `gitea generate secret INTERNAL_TOKEN` | +| `GITEA_ADMIN_USER` | `gitea-admin` | Must match `admins[0].username` in `users.yml` | +| `GITEA_ADMIN_PASS` | `Admin1234!` | Must match `admins[0].password` in `users.yml` | +| `POSTGRES_USER` | `gitea` | DB user | +| `POSTGRES_PASSWORD` | `gitea` | DB password | +| `POSTGRES_DB` | `gitea` | DB name | +| `HTTP_PORT` | `3000` | Host port for HTTP | +| `SSH_PORT` | `2222` | Host port for SSH (avoids conflict with host sshd) | + +--- + +## Troubleshooting + +**Provisioner exits immediately with "Already provisioned"** +Remove the stamp and re-run: `make reprovision` + +**`gitea admin user create` fails silently** +Check provisioner logs: `make logs-provisioner` +The stamp is NOT written on failure — restart the provisioner to retry. + +**SSH key injection fails (HTTP 422)** +The key already exists in Gitea, or the key format is invalid. +Verify key format with `ssh-keygen -l -f `. + +**Port 3000 already in use** +Set `HTTP_PORT=3001` (or any free port) in `.env`. diff --git a/03_container_layer/docker/admin/gitea/compose.yml b/03_container_layer/docker/admin/gitea/compose.yml new file mode 100644 index 0000000..d82025b --- /dev/null +++ b/03_container_layer/docker/admin/gitea/compose.yml @@ -0,0 +1,91 @@ +# +# ISSUE 141 +# + +services: + + db: + image: postgres:16-alpine + container_name: gitea-db + environment: + POSTGRES_USER: ${POSTGRES_USER:-gitea} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in .env} + POSTGRES_DB: ${POSTGRES_DB:-gitea} + volumes: + - gitea-db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-gitea}"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + gitea: &main + image: gitea/gitea:latest + container_name: gitea + environment: + USER_UID: "1000" + USER_GID: "1000" + GITEA__database__DB_TYPE: postgres + GITEA__database__HOST: db:5432 + GITEA__database__NAME: ${POSTGRES_DB:-gitea} + GITEA__database__USER: ${POSTGRES_USER:-gitea} + GITEA__database__PASSWD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in .env} + GITEA__server__DOMAIN: ${GITEA_DOMAIN:-localhost} + GITEA__server__ROOT_URL: ${GITEA_BASE_URL:-http://localhost:3000} + GITEA__server__HTTP_PORT: "3000" + GITEA__server__SSH_DOMAIN: ${GITEA_DOMAIN:-localhost} + GITEA__server__START_SSH_SERVER: "true" + GITEA__server__SSH_LISTEN_PORT: "22" + GITEA__server__SSH_PORT: "${SSH_PORT:-2222}" + GITEA__service__DISABLE_REGISTRATION: "true" + GITEA__security__INSTALL_LOCK: "true" + GITEA__security__SECRET_KEY: ${GITEA_SECRET_KEY:-please-change-me-in-production} + GITEA__security__INTERNAL_TOKEN: ${GITEA_INTERNAL_TOKEN:-please-change-me-in-production} + volumes: + - gitea-data:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "${HTTP_PORT:-3000}:3000" + - "${SSH_PORT:-2222}:22" + depends_on: + db: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/api/v1/version"] + interval: 15s + timeout: 10s + retries: 10 + start_period: 30s + restart: unless-stopped +# +# #### #### do not remove. +# +# debug: +# <<: *main +# container_name: gitea-debug +# ports: +# - "60443:443" +# command: ["sleep", "infinity"] + + provisioner: + build: + context: . + dockerfile: Dockerfile + container_name: gitea-provisioner + environment: + GITEA_URL: http://gitea:3000 + GITEA_ADMIN_USER: ${GITEA_ADMIN_USER:-gitea-admin} + GITEA_ADMIN_PASS: ${GITEA_ADMIN_PASS:-Admin1234!} + USERS_FILE: /provisioning/users.yml + volumes: + - gitea-data:/data + depends_on: + gitea: + condition: service_healthy + restart: "no" + +volumes: + gitea-data: + gitea-db-data: diff --git a/03_container_layer/docker/admin/gitea/provisioning/init.sh b/03_container_layer/docker/admin/gitea/provisioning/init.sh new file mode 100644 index 0000000..311f24c --- /dev/null +++ b/03_container_layer/docker/admin/gitea/provisioning/init.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env sh +# +# ISSUE 141 +# +# Bootstrap script for the Gitea provisioner sidecar. +# Runs once after Gitea is healthy; guarded by a stamp file for idempotency. +# +# User/SSH-key declarations come from USERS_FILE (default: /provisioning/users.yml). +# Admin users are created via the gitea CLI (direct DB access via app.ini). +# SSH keys are injected via the Gitea REST API. +# +set -eu + +GITEA_URL="${GITEA_URL:-http://gitea:3000}" +GITEA_ADMIN_USER="${GITEA_ADMIN_USER:-gitea-admin}" +GITEA_ADMIN_PASS="${GITEA_ADMIN_PASS:-Admin1234!}" +USERS_FILE="${USERS_FILE:-/provisioning/users.yml}" +GITEA_CONFIG="/data/gitea/conf/app.ini" +PROVISION_STAMP="/data/gitea/.provisioned" + +# ── 1. Wait for Gitea HTTP (max 180 s) ───────────────────────────────────── +echo "[init] Waiting for Gitea at ${GITEA_URL} ..." +attempts=0 +until curl -sf "${GITEA_URL}/api/v1/version" >/dev/null 2>&1; do + attempts=$((attempts + 1)) + if [ "${attempts}" -ge 60 ]; then + echo "[fatal] Gitea did not become healthy after 180 s. Aborting." + exit 1 + fi + sleep 3 +done +echo "[init] Gitea 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 (gitea 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}" + cli_out=$(gitea admin user create \ + --config "${GITEA_CONFIG}" \ + --admin \ + --username "${username}" \ + --password "${password}" \ + --email "${email}" \ + --must-change-password=false 2>&1) || { + case "${cli_out}" in + *"user already exists"*|*"name already exists"*) + echo "[warn] ${username} already exists — skipping" ;; + *) + echo "[error] Failed to create ${username}: ${cli_out}"; exit 1 ;; + esac + } + + i=$((i + 1)) +done + +# ── 4. Regular users (gitea 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}" + cli_out=$(gitea admin user create \ + --config "${GITEA_CONFIG}" \ + --username "${username}" \ + --password "${password}" \ + --email "${email}" \ + --must-change-password=false 2>&1) || { + case "${cli_out}" in + *"user already exists"*|*"name already exists"*) + echo "[warn] ${username} already exists — skipping" ;; + *) + echo "[error] Failed to create ${username}: ${cli_out}"; exit 1 ;; + esac + } + + i=$((i + 1)) +done + +# ── 5. SSH keys (REST API — first admin in users.yml acts as auth) ────────── +inject_keys() { + local section="${1}" + local count j k uname key_count key + + count=$(yq e ".${section} | length" "${USERS_FILE}") + j=0 + while [ "${j}" -lt "${count}" ]; do + uname=$(yq e ".${section}[${j}].username" "${USERS_FILE}") + key_count=$(yq e ".${section}[${j}].ssh_keys | length" "${USERS_FILE}") + + k=0 + while [ "${k}" -lt "${key_count}" ]; do + key=$(yq e ".${section}[${j}].ssh_keys[${k}]" "${USERS_FILE}") + echo "[init] + SSH key ${k} -> ${uname}" + # Use jq to build the JSON payload to avoid injection via crafted key strings. + payload=$(jq -n --arg k "${key}" --arg t "${uname}-key-${k}" \ + '{"key":$k,"read_only":false,"title":$t}') + curl -sf -X POST "${GITEA_URL}/api/v1/admin/users/${uname}/keys" \ + -u "${GITEA_ADMIN_USER}:${GITEA_ADMIN_PASS}" \ + -H "Content-Type: application/json" \ + -d "${payload}" \ + >/dev/null \ + || echo "[warn] SSH key ${k} for ${uname} may already exist — skipping" + k=$((k + 1)) + done + + j=$((j + 1)) + done +} + +echo "[init] Injecting SSH keys ..." +inject_keys admins +inject_keys users + +# ── 6. Mark as provisioned ────────────────────────────────────────────────── +touch "${PROVISION_STAMP}" +echo "[init] Provisioning complete." diff --git a/03_container_layer/docker/admin/gitea/provisioning/users.yml b/03_container_layer/docker/admin/gitea/provisioning/users.yml new file mode 100644 index 0000000..a415913 --- /dev/null +++ b/03_container_layer/docker/admin/gitea/provisioning/users.yml @@ -0,0 +1,33 @@ +# Declarative user provisioning for the Gitea 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. +# +# SSH public keys must be real ed25519 / rsa / ecdsa keys. +# Replace the placeholder key strings with actual public keys before deploying. +# +# To scale: add/remove entries under admins: or users: +# The provisioner is idempotent — re-running skips already-created accounts. + +admins: + - username: gitea-admin + email: admin@range42.local + password: "Admin1234!" + ssh_keys: + - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleAdminKeyReplaceMeBeforeDeployment admin@range42" + +users: + - username: trainee01 + email: trainee01@range42.local + password: "Trainee1234!" + ssh_keys: + - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleTrainee01KeyReplaceMeBeforeDeployment trainee01@range42" + - username: trainee02 + email: trainee02@range42.local + password: "Trainee1234!" + ssh_keys: [] + - username: trainee03 + email: trainee03@range42.local + password: "Trainee1234!" + ssh_keys: []