Files
gitea-migration/phase1_gitea_unraid.sh
2026-03-01 10:39:38 -05:00

278 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# =============================================================================
# phase1_gitea_unraid.sh — Deploy Gitea on Unraid
# Produces: Running Gitea instance, admin user, API token in .env, org created
# =============================================================================
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "${SCRIPT_DIR}/lib/common.sh"
load_env
require_vars UNRAID_IP UNRAID_SSH_USER UNRAID_SSH_PORT UNRAID_GITEA_DATA_PATH \
UNRAID_MACVLAN_PARENT UNRAID_MACVLAN_SUBNET UNRAID_MACVLAN_GATEWAY \
UNRAID_MACVLAN_IP_RANGE UNRAID_GITEA_IP \
GITEA_ADMIN_USER GITEA_ADMIN_PASSWORD GITEA_ADMIN_EMAIL \
GITEA_ORG_NAME GITEA_INSTANCE_NAME \
GITEA_DB_TYPE GITEA_VERSION \
GITEA_INTERNAL_URL GITEA_DOMAIN
if [[ "${GITEA_DB_TYPE}" != "sqlite3" ]]; then
require_vars GITEA_DB_HOST GITEA_DB_PORT GITEA_DB_NAME GITEA_DB_USER GITEA_DB_PASSWD
fi
phase_header 1 "Gitea on Unraid"
# Alias for shorter references in template rendering.
# Templates use $DATA_PATH as a generic variable name so the same template
# works for both Unraid (Phase 1) and Fedora (Phase 2).
DATA_PATH="$UNRAID_GITEA_DATA_PATH"
# ---------------------------------------------------------------------------
# DB-specific variables — set based on GITEA_DB_TYPE for template rendering.
# For sqlite3, the DB service blocks are stripped entirely.
# For external DBs, these vars populate the docker-compose DB service.
# ---------------------------------------------------------------------------
_set_db_vars() {
case "${GITEA_DB_TYPE}" in
postgres)
DB_DOCKER_IMAGE="postgres:16-alpine"
DB_ENV_VARS=" - POSTGRES_USER=${GITEA_DB_USER}
- POSTGRES_PASSWORD=${GITEA_DB_PASSWD}
- POSTGRES_DB=${GITEA_DB_NAME}"
DB_HEALTHCHECK='["CMD-SHELL", "pg_isready -U '"${GITEA_DB_USER}"'"]'
DB_DATA_DIR="postgresql/data"
;;
mysql)
DB_DOCKER_IMAGE="mysql:8.0"
DB_ENV_VARS=" - MYSQL_ROOT_PASSWORD=${GITEA_DB_PASSWD}
- MYSQL_DATABASE=${GITEA_DB_NAME}
- MYSQL_USER=${GITEA_DB_USER}
- MYSQL_PASSWORD=${GITEA_DB_PASSWD}"
DB_HEALTHCHECK='["CMD", "mysqladmin", "ping", "-h", "localhost"]'
DB_DATA_DIR="mysql"
;;
mssql)
DB_DOCKER_IMAGE="mcr.microsoft.com/mssql/server:2022-latest"
DB_ENV_VARS=" - ACCEPT_EULA=Y
- SA_PASSWORD=${GITEA_DB_PASSWD}"
# shellcheck disable=SC2089,SC2016
DB_HEALTHCHECK='["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '"'"'${GITEA_DB_PASSWD}'"'"' -Q \"SELECT 1\" -C -N"]'
DB_DATA_DIR="mssql/data"
;;
esac
# shellcheck disable=SC2090
export DB_DOCKER_IMAGE DB_ENV_VARS DB_HEALTHCHECK DB_DATA_DIR
}
# Strip conditional blocks from a rendered file.
# Usage: _strip_block <file> <start_marker> <end_marker>
_strip_block() {
local file="$1" start="$2" end="$3"
sed -i.bak "/${start}/,/${end}/d" "$file"
rm -f "${file}.bak"
}
# ---------------------------------------------------------------------------
# Step 1: Create data directories
# ---------------------------------------------------------------------------
log_step 1 "Creating data directories on Unraid..."
if ssh_exec UNRAID "test -d '${DATA_PATH}/data'"; then
log_info "Data directory already exists — skipping"
else
ssh_exec UNRAID "mkdir -p '${DATA_PATH}/data' '${DATA_PATH}/config'"
# Create DB data directory for external databases
if [[ "${GITEA_DB_TYPE}" != "sqlite3" ]]; then
ssh_exec UNRAID "mkdir -p '${DATA_PATH}/db'"
fi
log_success "Data directories created"
fi
# ---------------------------------------------------------------------------
# Step 2: Create macvlan Docker network (idempotent)
# Each container gets its own LAN IP — no host port mapping needed.
# ---------------------------------------------------------------------------
log_step 2 "Creating macvlan Docker network on Unraid..."
if ssh_exec UNRAID "docker network inspect gitea_net" &>/dev/null; then
log_info "gitea_net network already exists — skipping"
else
ssh_exec UNRAID "docker network create \
--driver macvlan \
--subnet='${UNRAID_MACVLAN_SUBNET}' \
--gateway='${UNRAID_MACVLAN_GATEWAY}' \
--ip-range='${UNRAID_MACVLAN_IP_RANGE}' \
-o parent='${UNRAID_MACVLAN_PARENT}' \
gitea_net"
log_success "macvlan network gitea_net created"
fi
# ---------------------------------------------------------------------------
# Step 3: Render + SCP docker-compose file
# ---------------------------------------------------------------------------
log_step 3 "Deploying docker-compose.yml..."
if ssh_exec UNRAID "test -f '${DATA_PATH}/docker-compose.yml'"; then
log_info "docker-compose.yml already exists — skipping"
else
TMPFILE=$(mktemp)
# Set variables for template — containers get dedicated LAN IPs via macvlan
GITEA_CONTAINER_IP="${UNRAID_GITEA_IP}"
export DATA_PATH GITEA_CONTAINER_IP
if [[ "$GITEA_DB_TYPE" == "sqlite3" ]]; then
# No DB service needed — render template then strip DB blocks
render_template "${SCRIPT_DIR}/templates/docker-compose-gitea.yml.tpl" "$TMPFILE" \
"\${GITEA_VERSION} \${DATA_PATH} \${GITEA_CONTAINER_IP}"
_strip_block "$TMPFILE" "DB_SERVICE_START" "DB_SERVICE_END"
_strip_block "$TMPFILE" "DB_DEPENDS_START" "DB_DEPENDS_END"
else
# External DB — set DB-specific vars then render
_set_db_vars
DB_CONTAINER_IP="${UNRAID_DB_IP}"
export DB_CONTAINER_IP
render_template "${SCRIPT_DIR}/templates/docker-compose-gitea.yml.tpl" "$TMPFILE" \
"\${GITEA_VERSION} \${DATA_PATH} \${GITEA_CONTAINER_IP} \${DB_DOCKER_IMAGE} \${DB_ENV_VARS} \${DB_DATA_DIR} \${DB_HEALTHCHECK} \${DB_CONTAINER_IP}"
fi
scp_to UNRAID "$TMPFILE" "${DATA_PATH}/docker-compose.yml"
rm -f "$TMPFILE"
log_success "docker-compose.yml deployed"
fi
# ---------------------------------------------------------------------------
# Step 4: Render + SCP app.ini
# ---------------------------------------------------------------------------
log_step 4 "Deploying app.ini..."
if ssh_exec UNRAID "test -f '${DATA_PATH}/config/app.ini'"; then
log_info "app.ini already exists — skipping"
else
TMPFILE=$(mktemp)
# Generate a random secret key for this instance
GITEA_SECRET_KEY=$(openssl rand -hex 32)
export GITEA_SECRET_KEY
if [[ "$GITEA_DB_TYPE" == "sqlite3" ]]; then
render_template "${SCRIPT_DIR}/templates/app.ini.tpl" "$TMPFILE" \
"\${GITEA_DOMAIN} \${GITEA_DB_TYPE} \${GITEA_SECRET_KEY}"
_strip_block "$TMPFILE" "EXTDB_BLOCK_START" "EXTDB_BLOCK_END"
else
render_template "${SCRIPT_DIR}/templates/app.ini.tpl" "$TMPFILE" \
"\${GITEA_DOMAIN} \${GITEA_DB_TYPE} \${GITEA_SECRET_KEY} \${GITEA_DB_HOST} \${GITEA_DB_PORT} \${GITEA_DB_NAME} \${GITEA_DB_USER} \${GITEA_DB_PASSWD}"
_strip_block "$TMPFILE" "SQLITE_BLOCK_START" "SQLITE_BLOCK_END"
fi
scp_to UNRAID "$TMPFILE" "${DATA_PATH}/config/app.ini"
rm -f "$TMPFILE"
log_success "app.ini deployed"
fi
# ---------------------------------------------------------------------------
# Step 5: Start Gitea container
# ---------------------------------------------------------------------------
log_step 5 "Starting Gitea container..."
CONTAINER_STATUS=$(ssh_exec UNRAID "docker ps --filter name=gitea --format '{{.Status}}'" 2>/dev/null || true)
if [[ "$CONTAINER_STATUS" == *"Up"* ]]; then
log_info "Gitea container already running — skipping"
else
# Try modern "docker compose" first (plugin), fall back to standalone "docker-compose"
ssh_exec UNRAID "cd '${DATA_PATH}' && docker compose up -d 2>/dev/null || docker-compose up -d"
log_success "Gitea container started"
fi
# ---------------------------------------------------------------------------
# Step 6: Wait for Gitea to be ready
# ---------------------------------------------------------------------------
log_step 6 "Waiting for Gitea to be ready..."
wait_for_http "${GITEA_INTERNAL_URL}/api/v1/version" 120
# ---------------------------------------------------------------------------
# Step 7: Create admin user
# ---------------------------------------------------------------------------
log_step 7 "Creating admin user..."
if curl -sf -u "${GITEA_ADMIN_USER}:${GITEA_ADMIN_PASSWORD}" "${GITEA_INTERNAL_URL}/api/v1/user" -o /dev/null 2>/dev/null; then
log_info "Admin user already exists — skipping"
else
# Create admin via CLI inside the container (not the API) because no API
# token exists yet at this point. || true prevents set -e from exiting if
# the user was partially created in a previous interrupted run.
ssh_exec UNRAID "docker exec gitea gitea admin user create \
--username '${GITEA_ADMIN_USER}' \
--password '${GITEA_ADMIN_PASSWORD}' \
--email '${GITEA_ADMIN_EMAIL}' \
--admin \
--must-change-password=false" || true
# Verify with API call — this is the real success check
if curl -sf -u "${GITEA_ADMIN_USER}:${GITEA_ADMIN_PASSWORD}" "${GITEA_INTERNAL_URL}/api/v1/user" -o /dev/null 2>/dev/null; then
log_success "Admin user created"
else
log_error "Failed to create admin user"
exit 1
fi
fi
# ---------------------------------------------------------------------------
# Step 8: Generate API token and save to .env
# ---------------------------------------------------------------------------
log_step 8 "Generating API token..."
if [[ -n "${GITEA_ADMIN_TOKEN:-}" ]]; then
# Verify existing token works
if curl -sf -H "Authorization: token ${GITEA_ADMIN_TOKEN}" "${GITEA_INTERNAL_URL}/api/v1/user" -o /dev/null 2>/dev/null; then
log_info "API token already exists and is valid — skipping"
else
log_warn "Existing token is invalid — generating new one"
GITEA_ADMIN_TOKEN=""
fi
fi
if [[ -z "${GITEA_ADMIN_TOKEN:-}" ]]; then
# Delete any stale token with the same name (idempotent re-run safety).
# Gitea returns 409 Conflict if a token with this name already exists.
curl -sf -u "${GITEA_ADMIN_USER}:${GITEA_ADMIN_PASSWORD}" \
-X DELETE \
"${GITEA_INTERNAL_URL}/api/v1/users/${GITEA_ADMIN_USER}/tokens/migration-token" \
-o /dev/null 2>/dev/null || true
# Generate token using basic auth (username:password).
# The .sha1 field in the response contains the full token — this is the
# ONLY time it's returned, so we must save it immediately.
TOKEN_RESPONSE=$(curl -sf -u "${GITEA_ADMIN_USER}:${GITEA_ADMIN_PASSWORD}" \
-X POST \
-H "Content-Type: application/json" \
-d '{"name":"migration-token","scopes":["all"]}' \
"${GITEA_INTERNAL_URL}/api/v1/users/${GITEA_ADMIN_USER}/tokens")
GITEA_ADMIN_TOKEN=$(printf '%s' "$TOKEN_RESPONSE" | jq -r '.sha1')
if [[ -z "$GITEA_ADMIN_TOKEN" ]] || [[ "$GITEA_ADMIN_TOKEN" == "null" ]]; then
log_error "Failed to generate API token"
exit 1
fi
save_env_var "GITEA_ADMIN_TOKEN" "$GITEA_ADMIN_TOKEN"
log_success "API token generated and saved to .env"
fi
# ---------------------------------------------------------------------------
# Step 9: Create organization
# ---------------------------------------------------------------------------
log_step 9 "Creating organization '${GITEA_ORG_NAME}'..."
if curl -sf -H "Authorization: token ${GITEA_ADMIN_TOKEN}" "${GITEA_INTERNAL_URL}/api/v1/orgs/${GITEA_ORG_NAME}" -o /dev/null 2>/dev/null; then
log_info "Organization already exists — skipping"
else
curl -sf -X POST \
-H "Authorization: token ${GITEA_ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"username\":\"${GITEA_ORG_NAME}\",\"full_name\":\"${GITEA_INSTANCE_NAME}\",\"visibility\":\"public\"}" \
"${GITEA_INTERNAL_URL}/api/v1/orgs" -o /dev/null
if curl -sf -H "Authorization: token ${GITEA_ADMIN_TOKEN}" "${GITEA_INTERNAL_URL}/api/v1/orgs/${GITEA_ORG_NAME}" -o /dev/null 2>/dev/null; then
log_success "Organization created"
else
log_error "Failed to create organization"
exit 1
fi
fi
log_success "Phase 1 complete — Gitea is running on Unraid"