learning_ai_common_plat/scripts/gitea/bootstrap-vm.sh
saravanakumardb1 678d8df42c feat(gitea): add bootstrap-vm.sh for fresh cloud VM setup
Idempotent end-to-end Gitea bootstrap for Azure VM (or any Linux host
with Docker available). Replaces manual SSH-and-paste workflow.

Steps (each skippable on re-run):
  1. Install Docker via official script (skip with --skip-docker)
  2. Write /etc/gitea/docker-compose.yml with package registry enabled
  3. Start gitea container, wait for HTTP :3300
  4. Create admin user via 'gitea admin user create' (CLI inside container,
     no auth bootstrap needed)
  5. Create npm-user (learning_ai_user) via admin API
  6. Mint npm-scoped token with write:package + read:package

Two execution modes:
  - On the VM directly: scp + ssh + run
  - Locally targeting remote: --ssh-host azureuser@vm

Outputs npm token to --output FILE or stdout. Prints copy-paste-ready
command for writing to ~/.gitea_npm_token_home on the workstation.

Final summary prints the doctor.sh verification command so user can
confirm registry reachability from their laptop in one step.

--dry-run shows planned actions without execution.
--force re-creates users (use after manual deletion).

Closes the 'cloud VM bootstrap' gap identified during the Gitea hardening
review — pairs with scripts/gitea/{doctor,token}.sh from commit 610a59fd.
2026-05-27 01:20:56 -07:00

374 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
# bootstrap-vm.sh — Idempotent Gitea bootstrap for a fresh (or existing) cloud VM.
#
# Goes from "VM running with Docker installed" → "Gitea running on :3300
# with admin + learning_ai_user accounts + npm-scoped token written to file".
#
# This script can run in two modes:
#
# 1. ON THE VM directly (recommended): SCP this file to the VM, SSH in, run.
# scp scripts/gitea/bootstrap-vm.sh azureuser@<vm>:~/
# ssh azureuser@<vm> 'bash bootstrap-vm.sh'
#
# 2. LOCALLY targeting a remote VM via SSH:
# bash scripts/gitea/bootstrap-vm.sh --ssh-host azureuser@<vm>
#
# Usage:
# bash bootstrap-vm.sh [OPTIONS]
#
# Options:
# --admin-user NAME Gitea admin username (default: gitea-admin)
# --admin-email EMAIL Gitea admin email (default: admin@bytelyst.local)
# --admin-pass PASS Gitea admin password (default: generated, printed)
# --npm-user NAME Owner for @bytelyst packages (default: learning_ai_user)
# --npm-email EMAIL Email for npm-user (default: npm@bytelyst.local)
# --gitea-version VER Gitea Docker tag (default: 1.22)
# --data-dir PATH Host path for Gitea data (default: /var/lib/gitea)
# --output FILE Write npm token to FILE (default: print to stdout
# with a fenced block for easy copy)
# --ssh-host HOST Run remotely via SSH (e.g. azureuser@10.0.0.4)
# --skip-docker Skip Docker install (assume present)
# --dry-run Print actions without executing
# --force Re-create admin/npm user even if they exist
# -h, --help Show this help
#
# Exit codes:
# 0 bootstrap succeeded
# 1 bootstrap failed at some step (error printed)
# 2 bad arguments
#
# Side effects (idempotent unless --force):
# - Installs Docker (skipped if present, or with --skip-docker)
# - Creates /var/lib/gitea (or --data-dir) — persistent across restarts
# - Writes /etc/gitea/docker-compose.yml
# - Starts/restarts `gitea` container on port 3300
# - Creates admin user (skipped if exists)
# - Creates npm-user via admin API (skipped if exists)
# - Mints a new npm-scoped token (always — old token NOT revoked,
# just supplemented; rotate via scripts/gitea/token.sh rotate)
# - Writes token to --output FILE or stdout
#
# Re-runnable safely. Re-running:
# - Detects existing Gitea, skips install
# - Detects existing users, skips creation (unless --force)
# - Always mints a fresh token (each run = new token; old ones still valid
# unless you revoke them in Gitea UI)
set -uo pipefail
# ── Defaults ────────────────────────────────────────────────────
ADMIN_USER="gitea-admin"
ADMIN_EMAIL="admin@bytelyst.local"
ADMIN_PASS=""
NPM_USER="learning_ai_user"
NPM_EMAIL="npm@bytelyst.local"
GITEA_VERSION="1.22"
DATA_DIR="/var/lib/gitea"
OUTPUT_FILE=""
SSH_HOST=""
SKIP_DOCKER=false
DRY_RUN=false
FORCE=false
GITEA_PORT="3300"
GITEA_SSH_PORT="222"
while [ $# -gt 0 ]; do
case "$1" in
--admin-user) ADMIN_USER="$2"; shift 2 ;;
--admin-email) ADMIN_EMAIL="$2"; shift 2 ;;
--admin-pass) ADMIN_PASS="$2"; shift 2 ;;
--npm-user) NPM_USER="$2"; shift 2 ;;
--npm-email) NPM_EMAIL="$2"; shift 2 ;;
--gitea-version) GITEA_VERSION="$2"; shift 2 ;;
--data-dir) DATA_DIR="$2"; shift 2 ;;
--output) OUTPUT_FILE="$2"; shift 2 ;;
--ssh-host) SSH_HOST="$2"; shift 2 ;;
--skip-docker) SKIP_DOCKER=true; shift ;;
--dry-run) DRY_RUN=true; shift ;;
--force) FORCE=true; shift ;;
-h|--help) sed -n '2,42p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "Unknown arg: $1" >&2; exit 2 ;;
esac
done
# ── Remote re-exec via SSH ──────────────────────────────────────
if [ -n "$SSH_HOST" ]; then
echo "→ Re-executing on $SSH_HOST via SSH"
# Copy this script to the remote and re-run with same args minus --ssh-host
ARGS=()
[ "$ADMIN_USER" != "gitea-admin" ] && ARGS+=(--admin-user "$ADMIN_USER")
[ "$ADMIN_EMAIL" != "admin@bytelyst.local" ] && ARGS+=(--admin-email "$ADMIN_EMAIL")
[ -n "$ADMIN_PASS" ] && ARGS+=(--admin-pass "$ADMIN_PASS")
[ "$NPM_USER" != "learning_ai_user" ] && ARGS+=(--npm-user "$NPM_USER")
[ "$NPM_EMAIL" != "npm@bytelyst.local" ] && ARGS+=(--npm-email "$NPM_EMAIL")
[ "$GITEA_VERSION" != "1.22" ] && ARGS+=(--gitea-version "$GITEA_VERSION")
[ "$DATA_DIR" != "/var/lib/gitea" ] && ARGS+=(--data-dir "$DATA_DIR")
$SKIP_DOCKER && ARGS+=(--skip-docker)
$DRY_RUN && ARGS+=(--dry-run)
$FORCE && ARGS+=(--force)
scp "$0" "$SSH_HOST:/tmp/bootstrap-vm.sh" >/dev/null
ssh "$SSH_HOST" "bash /tmp/bootstrap-vm.sh ${ARGS[*]}"
exit $?
fi
# ── Helpers ─────────────────────────────────────────────────────
say() { echo "$1"; }
ok() { echo "$1"; }
err() { echo "$1" >&2; exit 1; }
run() {
if $DRY_RUN; then
echo " [dry-run] $*"
else
eval "$@"
fi
}
# Generate random password if not set
gen_password() {
# 24 chars, alphanumeric (safe for shell + URLs)
if command -v openssl >/dev/null 2>&1; then
openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 24
else
tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 24
fi
}
if [ -z "$ADMIN_PASS" ]; then
ADMIN_PASS="$(gen_password)"
PASS_GENERATED=true
else
PASS_GENERATED=false
fi
# Detect sudo
SUDO=""
if [ "$(id -u)" -ne 0 ]; then
SUDO="sudo"
fi
# ── Step 1: Install Docker ──────────────────────────────────────
step_docker() {
say "Step 1/6: Docker"
if $SKIP_DOCKER; then
ok "Skipped (--skip-docker)"
return
fi
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
ok "Docker + compose plugin already installed ($(docker --version))"
return
fi
say " Installing Docker via official convenience script…"
run "curl -fsSL https://get.docker.com | $SUDO sh"
run "$SUDO usermod -aG docker \"\$USER\" || true"
ok "Docker installed (you may need to re-login for group membership)"
}
# ── Step 2: Write docker-compose.yml ────────────────────────────
step_compose() {
say "Step 2/6: Gitea docker-compose"
run "$SUDO mkdir -p $DATA_DIR /etc/gitea"
local compose="/etc/gitea/docker-compose.yml"
run "$SUDO tee $compose >/dev/null <<EOF
services:
gitea:
image: gitea/gitea:$GITEA_VERSION
container_name: gitea
restart: unless-stopped
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__server__DOMAIN=\$(hostname)
- GITEA__server__ROOT_URL=http://\$(hostname):$GITEA_PORT/
- GITEA__server__HTTP_PORT=3000
- GITEA__server__SSH_PORT=$GITEA_SSH_PORT
- GITEA__service__DISABLE_REGISTRATION=true
- GITEA__packages__ENABLED=true
ports:
- \"$GITEA_PORT:3000\"
- \"$GITEA_SSH_PORT:22\"
volumes:
- $DATA_DIR:/data
EOF"
ok "Wrote $compose"
}
# ── Step 3: Start Gitea + wait for ready ────────────────────────
step_start() {
say "Step 3/6: Start Gitea container"
run "cd /etc/gitea && $SUDO docker compose up -d"
if $DRY_RUN; then
ok "Would start gitea container"
return
fi
say " Waiting for Gitea HTTP on :$GITEA_PORT (up to 60s)…"
local i=0
while [ $i -lt 30 ]; do
if curl -fsS "http://localhost:$GITEA_PORT/api/v1/version" >/dev/null 2>&1; then
ok "Gitea reachable ($(curl -fsS "http://localhost:$GITEA_PORT/api/v1/version"))"
return
fi
sleep 2
i=$((i+1))
done
err "Gitea did not come up within 60s. Check: $SUDO docker logs gitea"
}
# ── Step 4: Create admin user via Gitea CLI ─────────────────────
step_admin() {
say "Step 4/6: Admin user '$ADMIN_USER'"
if $DRY_RUN; then
echo " [dry-run] $SUDO docker exec gitea gitea admin user create --admin ..."
return
fi
# Check if admin already exists
if $SUDO docker exec gitea gitea admin user list 2>/dev/null | awk 'NR>1 {print $2}' | grep -qx "$ADMIN_USER"; then
if $FORCE; then
say " --force: re-setting admin password"
$SUDO docker exec gitea gitea admin user change-password \
--username "$ADMIN_USER" --password "$ADMIN_PASS" >/dev/null
ok "Admin password reset"
else
ok "Admin '$ADMIN_USER' already exists — leaving password unchanged"
PASS_GENERATED=false # don't claim a fresh password if we didn't set one
fi
else
$SUDO docker exec gitea gitea admin user create \
--username "$ADMIN_USER" \
--password "$ADMIN_PASS" \
--email "$ADMIN_EMAIL" \
--admin \
--must-change-password=false >/dev/null
ok "Admin '$ADMIN_USER' created"
fi
}
# ── Step 5: Create npm-user via API ─────────────────────────────
step_npm_user() {
say "Step 5/6: NPM owner '$NPM_USER'"
if $DRY_RUN; then
echo " [dry-run] curl -u $ADMIN_USER:**** POST /api/v1/admin/users"
return
fi
local base="http://localhost:$GITEA_PORT/api/v1"
local code
code=$(curl -fsS -o /tmp/gitea-user-check.json -w "%{http_code}" \
-u "$ADMIN_USER:$ADMIN_PASS" "$base/users/$NPM_USER" 2>/dev/null || echo "000")
if [ "$code" = "200" ] && ! $FORCE; then
ok "NPM user '$NPM_USER' already exists"
else
local npm_pass; npm_pass="$(gen_password)"
local resp
resp=$(curl -fsS -u "$ADMIN_USER:$ADMIN_PASS" \
-X POST "$base/admin/users" \
-H 'Content-Type: application/json' \
-d "{\"username\":\"$NPM_USER\",\"email\":\"$NPM_EMAIL\",\"password\":\"$npm_pass\",\"must_change_password\":false}" 2>&1)
if echo "$resp" | grep -q '"id"'; then
ok "NPM user '$NPM_USER' created"
else
err "Failed to create NPM user: $resp"
fi
fi
}
# ── Step 6: Mint npm token ──────────────────────────────────────
step_token() {
say "Step 6/6: Mint npm token for '$NPM_USER'"
if $DRY_RUN; then
echo " [dry-run] curl -u $NPM_USER POST /api/v1/users/$NPM_USER/tokens"
NPM_TOKEN="DRY_RUN_TOKEN_PLACEHOLDER"
return
fi
# We need the npm-user's password to call /tokens — but we only have it
# at create time. Workaround: use admin Basic Auth to call the admin-only
# `/admin/users/<u>/tokens` endpoint? Gitea does not expose this.
#
# Approach: temporarily reset npm-user's password via admin, mint via
# the npm-user's own creds, then leave the password as-is (it's only used
# for token minting; the npm flow uses the token).
local npm_pass; npm_pass="$(gen_password)"
$SUDO docker exec gitea gitea admin user change-password \
--username "$NPM_USER" --password "$npm_pass" >/dev/null 2>&1 || true
local token_name="npm-bootstrap-$(date +%Y%m%d-%H%M%S)"
local base="http://localhost:$GITEA_PORT/api/v1"
local resp
resp=$(curl -fsS -u "$NPM_USER:$npm_pass" \
-X POST "$base/users/$NPM_USER/tokens" \
-H 'Content-Type: application/json' \
-d "{\"name\":\"$token_name\",\"scopes\":[\"write:package\",\"read:package\"]}" 2>&1)
NPM_TOKEN=$(echo "$resp" | grep -oE '"(sha1|token)":"[^"]+' | head -1 | sed 's/.*":"//')
if [ -z "$NPM_TOKEN" ]; then
err "Failed to mint token: $resp"
fi
ok "Token minted (name=$token_name, ${#NPM_TOKEN} chars)"
}
# ── Output token ────────────────────────────────────────────────
output_token() {
if [ -n "$OUTPUT_FILE" ]; then
if $DRY_RUN; then
echo "[dry-run] would write token to $OUTPUT_FILE"
else
printf '%s' "$NPM_TOKEN" > "$OUTPUT_FILE"
chmod 600 "$OUTPUT_FILE"
ok "Token written to $OUTPUT_FILE (mode 600)"
fi
fi
}
# ── Final summary ───────────────────────────────────────────────
summary() {
echo ""
echo "════════════════════════════════════════════════════════════════"
echo " ✅ Gitea bootstrap complete"
echo "════════════════════════════════════════════════════════════════"
echo ""
echo " Gitea URL: http://$(hostname):$GITEA_PORT"
echo " Admin user: $ADMIN_USER"
if $PASS_GENERATED && [ -z "$OUTPUT_FILE" ]; then
echo " Admin password: $ADMIN_PASS ← SAVE THIS NOW (won't be shown again)"
fi
echo " NPM owner: $NPM_USER"
echo " Registry URL: http://$(hostname):$GITEA_PORT/api/packages/$NPM_USER/npm/"
echo ""
if [ -z "$OUTPUT_FILE" ]; then
echo " ── NPM TOKEN (copy this NOW) ──"
echo ""
echo " $NPM_TOKEN"
echo ""
echo " Save to ~/.gitea_npm_token_home on your workstation:"
echo ""
echo " echo -n '$NPM_TOKEN' > ~/.gitea_npm_token_home"
echo " chmod 600 ~/.gitea_npm_token_home"
echo ""
else
echo " NPM token: written to $OUTPUT_FILE"
echo ""
fi
echo " Verify (from workstation):"
echo ""
echo " GITEA_NPM_HOST=$(hostname) GITEA_NPM_OWNER=$NPM_USER \\"
echo " GITEA_NPM_TOKEN=\$(cat ~/.gitea_npm_token_home) \\"
echo " bash scripts/gitea/doctor.sh"
echo ""
}
# ── Main ────────────────────────────────────────────────────────
say "Gitea VM bootstrap"
say " Host: $(hostname)"
say " Admin user: $ADMIN_USER"
say " NPM owner: $NPM_USER"
say " Gitea version: $GITEA_VERSION"
say " Data dir: $DATA_DIR"
say " Dry-run: $DRY_RUN"
echo ""
step_docker
step_compose
step_start
step_admin
step_npm_user
step_token
output_token
summary