215 lines
8.6 KiB
Bash
Executable file
215 lines
8.6 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# =============================================================================
|
|
# lib/init/nomad/forgejo-bootstrap.sh — Bootstrap Forgejo admin user
|
|
#
|
|
# Part of the Nomad+Vault migration (S2.4, issue #1069). Creates the
|
|
# disinto-admin user in Forgejo if it doesn't exist, enabling:
|
|
# - First-login success without manual intervention
|
|
# - PAT generation via API (required for disinto backup import #1058)
|
|
#
|
|
# The script is idempotent — re-running after success is a no-op.
|
|
#
|
|
# Scope:
|
|
# - Checks if user 'disinto-admin' exists via GET /api/v1/users/search
|
|
# - If not: POST /api/v1/admin/users to create admin user
|
|
# - Uses FORGE_ADMIN_PASS from environment (required)
|
|
#
|
|
# Idempotency contract:
|
|
# - User 'disinto-admin' exists → skip creation, log
|
|
# "[forgejo-bootstrap] admin user already exists"
|
|
# - User creation fails with "user already exists" → treat as success
|
|
#
|
|
# Preconditions:
|
|
# - Forgejo reachable at $FORGE_URL (default: http://127.0.0.1:3000)
|
|
# - Forgejo admin token at $FORGE_TOKEN (from Vault or env)
|
|
# - FORGE_ADMIN_PASS set (env var with admin password)
|
|
#
|
|
# Requires:
|
|
# - curl, jq
|
|
#
|
|
# Usage:
|
|
# lib/init/nomad/forgejo-bootstrap.sh
|
|
# lib/init/nomad/forgejo-bootstrap.sh --dry-run
|
|
#
|
|
# Exit codes:
|
|
# 0 success (user created + ready, or already exists)
|
|
# 1 precondition / API failure
|
|
# =============================================================================
|
|
set -euo pipefail
|
|
|
|
# ── Configuration ────────────────────────────────────────────────────────────
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
|
|
|
# shellcheck source=../../../lib/hvault.sh
|
|
source "${REPO_ROOT}/lib/hvault.sh"
|
|
|
|
# Configuration
|
|
FORGE_URL="${FORGE_URL:-http://127.0.0.1:3000}"
|
|
FORGE_TOKEN="${FORGE_TOKEN:-}"
|
|
FORGE_ADMIN_USER="${DISINTO_ADMIN_USER:-disinto-admin}"
|
|
FORGE_ADMIN_EMAIL="${DISINTO_ADMIN_EMAIL:-admin@disinto.local}"
|
|
|
|
# Derive FORGE_ADMIN_PASS from common env var patterns
|
|
# Priority: explicit FORGE_ADMIN_PASS > DISINTO_FORGE_ADMIN_PASS > FORGEJO_ADMIN_PASS
|
|
FORGE_ADMIN_PASS="${FORGE_ADMIN_PASS:-${DISINTO_FORGE_ADMIN_PASS:-${FORGEJO_ADMIN_PASS:-}}}"
|
|
|
|
LOG_TAG="[forgejo-bootstrap]"
|
|
log() { printf '%s %s\n' "$LOG_TAG" "$*" >&2; }
|
|
die() { printf '%s ERROR: %s\n' "$LOG_TAG" "$*" >&2; exit 1; }
|
|
|
|
# ── Flag parsing ─────────────────────────────────────────────────────────────
|
|
DRY_RUN="${DRY_RUN:-0}"
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--dry-run) DRY_RUN=1 ;;
|
|
-h|--help)
|
|
printf 'Usage: %s [--dry-run]\n\n' "$(basename "$0")"
|
|
printf 'Bootstrap Forgejo admin user if it does not exist.\n'
|
|
printf 'Idempotent: re-running is a no-op.\n\n'
|
|
printf 'Environment:\n'
|
|
printf ' FORGE_URL Forgejo base URL (default: http://127.0.0.1:3000)\n'
|
|
printf ' FORGE_TOKEN Forgejo admin token (from Vault or env)\n'
|
|
printf ' FORGE_ADMIN_PASS Admin password (required)\n'
|
|
printf ' DISINTO_ADMIN_USER Username for admin account (default: disinto-admin)\n'
|
|
printf ' DISINTO_ADMIN_EMAIL Admin email (default: admin@disinto.local)\n\n'
|
|
printf ' --dry-run Print planned actions without modifying Forgejo.\n'
|
|
exit 0
|
|
;;
|
|
*) die "invalid argument: ${arg} (try --help)" ;;
|
|
esac
|
|
done
|
|
|
|
# ── Precondition checks ──────────────────────────────────────────────────────
|
|
log "── Precondition check ──"
|
|
|
|
if [ -z "$FORGE_URL" ]; then
|
|
die "FORGE_URL is not set"
|
|
fi
|
|
|
|
if [ -z "$FORGE_ADMIN_PASS" ]; then
|
|
die "FORGE_ADMIN_PASS is not set (required for admin user creation)"
|
|
fi
|
|
|
|
# Resolve FORGE_TOKEN from Vault if not set in env
|
|
if [ -z "$FORGE_TOKEN" ]; then
|
|
log "reading FORGE_TOKEN from Vault at kv/disinto/shared/forge/token"
|
|
_hvault_default_env
|
|
token_raw="$(hvault_get_or_empty "kv/data/disinto/shared/forge/token" 2>/dev/null)" || true
|
|
if [ -n "$token_raw" ]; then
|
|
FORGE_TOKEN="$(printf '%s' "$token_raw" | jq -r '.data.data.token // empty' 2>/dev/null)" || true
|
|
fi
|
|
if [ -z "$FORGE_TOKEN" ]; then
|
|
die "FORGE_TOKEN not set and not found in Vault"
|
|
fi
|
|
log "forge token loaded from Vault"
|
|
fi
|
|
|
|
# ── Step 1/3: Check if admin user already exists ─────────────────────────────
|
|
log "── Step 1/3: check if admin user '${FORGE_ADMIN_USER}' exists ──"
|
|
|
|
# Use exact match via GET /api/v1/users/{username} (returns 404 if absent)
|
|
user_lookup_raw=$(curl -sf --max-time 10 \
|
|
"${FORGE_URL}/api/v1/users/${FORGE_ADMIN_USER}" 2>/dev/null) || {
|
|
# 404 means user doesn't exist
|
|
if [ $? -eq 7 ]; then
|
|
log "admin user '${FORGE_ADMIN_USER}' not found"
|
|
admin_user_exists=false
|
|
user_id=""
|
|
else
|
|
# Other curl errors (e.g., network, Forgejo down)
|
|
log "warning: failed to lookup user (Forgejo may not be ready yet)"
|
|
admin_user_exists=false
|
|
user_id=""
|
|
fi
|
|
}
|
|
|
|
if [ -n "$user_lookup_raw" ]; then
|
|
admin_user_exists=true
|
|
user_id=$(printf '%s' "$user_lookup_raw" | jq -r '.id // empty' 2>/dev/null) || true
|
|
if [ -n "$user_id" ]; then
|
|
log "admin user '${FORGE_ADMIN_USER}' already exists (user_id: ${user_id})"
|
|
fi
|
|
fi
|
|
|
|
# ── Step 2/3: Create admin user if needed ────────────────────────────────────
|
|
if [ "$admin_user_exists" = false ]; then
|
|
log "creating admin user '${FORGE_ADMIN_USER}'"
|
|
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
log "[dry-run] would create admin user with:"
|
|
log "[dry-run] username: ${FORGE_ADMIN_USER}"
|
|
log "[dry-run] email: ${FORGE_ADMIN_EMAIL}"
|
|
log "[dry-run] admin: true"
|
|
log "[dry-run] must_change_password: false"
|
|
else
|
|
# Create the admin user via the admin API
|
|
create_response=$(curl -sf --max-time 30 -X POST \
|
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
"${FORGE_URL}/api/v1/admin/users" \
|
|
-d "{
|
|
\"username\": \"${FORGE_ADMIN_USER}\",
|
|
\"email\": \"${FORGE_ADMIN_EMAIL}\",
|
|
\"password\": \"${FORGE_ADMIN_PASS}\",
|
|
\"admin\": true,
|
|
\"must_change_password\": false
|
|
}" 2>/dev/null) || {
|
|
# Check if the error is "user already exists" (race condition on re-run)
|
|
error_body=$(curl -s --max-time 30 -X POST \
|
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
"${FORGE_URL}/api/v1/admin/users" \
|
|
-d "{\"username\": \"${FORGE_ADMIN_USER}\", \"email\": \"${FORGE_ADMIN_EMAIL}\", \"password\": \"${FORGE_ADMIN_PASS}\", \"admin\": true, \"must_change_password\": false}" 2>/dev/null) || error_body=""
|
|
|
|
if echo "$error_body" | grep -q '"message".*"user already exists"'; then
|
|
log "admin user '${FORGE_ADMIN_USER}' already exists (race condition handled)"
|
|
admin_user_exists=true
|
|
else
|
|
die "failed to create admin user in Forgejo: ${error_body:-unknown error}"
|
|
fi
|
|
}
|
|
|
|
# Extract user_id from response
|
|
user_id=$(printf '%s' "$create_response" | jq -r '.id // empty' 2>/dev/null) || true
|
|
if [ -n "$user_id" ]; then
|
|
admin_user_exists=true
|
|
log "admin user '${FORGE_ADMIN_USER}' created (user_id: ${user_id})"
|
|
else
|
|
die "failed to extract user_id from Forgejo response"
|
|
fi
|
|
fi
|
|
else
|
|
log "admin user '${FORGE_ADMIN_USER}' already exists — skipping creation"
|
|
fi
|
|
|
|
# ── Step 3/3: Verify user was created and is admin ───────────────────────────
|
|
log "── Step 3/3: verify admin user is properly configured ──"
|
|
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
log "[dry-run] would verify admin user configuration"
|
|
log "done — [dry-run] complete"
|
|
else
|
|
# Verify the user exists and is admin
|
|
verify_response=$(curl -sf --max-time 10 \
|
|
-u "${FORGE_ADMIN_USER}:${FORGE_ADMIN_PASS}" \
|
|
"${FORGE_URL}/api/v1/user" 2>/dev/null) || {
|
|
die "failed to verify admin user credentials"
|
|
}
|
|
|
|
is_admin=$(printf '%s' "$verify_response" | jq -r '.is_admin // false' 2>/dev/null) || true
|
|
login=$(printf '%s' "$verify_response" | jq -r '.login // empty' 2>/dev/null) || true
|
|
|
|
if [ "$is_admin" != "true" ]; then
|
|
die "admin user '${FORGE_ADMIN_USER}' is not marked as admin"
|
|
fi
|
|
|
|
if [ "$login" != "$FORGE_ADMIN_USER" ]; then
|
|
die "admin user login mismatch: expected '${FORGE_ADMIN_USER}', got '${login}'"
|
|
fi
|
|
|
|
log "admin user verified: login=${login}, is_admin=${is_admin}"
|
|
log "done — Forgejo admin user is ready"
|
|
fi
|
|
|
|
exit 0
|