From 678d8df42cb7deb66a1f8a913b6d89bd1fd48011 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Wed, 27 May 2026 01:20:56 -0700 Subject: [PATCH] feat(gitea): add bootstrap-vm.sh for fresh cloud VM setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- scripts/gitea/bootstrap-vm.sh | 373 ++++++++++++++++++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100755 scripts/gitea/bootstrap-vm.sh diff --git a/scripts/gitea/bootstrap-vm.sh b/scripts/gitea/bootstrap-vm.sh new file mode 100755 index 00000000..ca1d0726 --- /dev/null +++ b/scripts/gitea/bootstrap-vm.sh @@ -0,0 +1,373 @@ +#!/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@:~/ +# ssh azureuser@ 'bash bootstrap-vm.sh' +# +# 2. LOCALLY targeting a remote VM via SSH: +# bash scripts/gitea/bootstrap-vm.sh --ssh-host azureuser@ +# +# 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 </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//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