#!/usr/bin/env bash set -Eeuo pipefail DEPLOY_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" cd "$DEPLOY_ROOT" declare -a COMPOSE_FILES=(-f compose.yaml) fail() { printf 'ERROR: %s\n' "$*" >&2 exit 1 } read_env_setting() { local key="$1" local value value="$( sed -n "s/^[[:space:]]*${key}[[:space:]]*=[[:space:]]*//p" .env | tail -n 1 )" value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" value="${value#\"}" value="${value%\"}" value="${value#\'}" value="${value%\'}" printf '%s' "$value" } set_env_setting() { local key="$1" local value="$2" local temporary_file temporary_file="$(mktemp "${DEPLOY_ROOT}/.env.tmp.XXXXXX")" || fail "unable to create temporary environment file" awk -v key="$key" -v value="$value" ' BEGIN { replaced = 0 } $0 ~ "^[[:space:]]*" key "[[:space:]]*=" { if (!replaced) { print key "=" value replaced = 1 } next } { print } END { if (!replaced) print key "=" value } ' .env >"$temporary_file" chmod 600 "$temporary_file" mv -f "$temporary_file" .env } select_compose_stack() { COMPOSE_FILES=(-f compose.yaml) if [[ "$SSL_MODE" == "none" ]]; then COMPOSE_FILES+=(-f compose.http.yaml) else COMPOSE_FILES+=(-f compose.tls.yaml) fi } compose_stack() { docker compose "${COMPOSE_FILES[@]}" "$@" } require_regular_file() { local path="$1" if [[ -f "$path" ]]; then return 0 fi if [[ -d "$path" ]]; then fail "${path} is a directory; remove it and restore the missing repository file" fi fail "required deployment file is missing: ${path}" } require_stack_files() { require_regular_file compose.yaml if [[ "$SSL_MODE" == "none" ]]; then require_regular_file compose.http.yaml require_regular_file deploy/nginx/templates/http.conf.template else require_regular_file compose.tls.yaml require_regular_file deploy/nginx/templates/tls.conf.template fi } broker_config_ready() { local key local value local -a required=( TAPM_GITEA_CLIENT_ID TAPM_GITEA_CLIENT_SECRET TAPM_GITEA_PACKAGE_USERNAME TAPM_GITEA_PACKAGE_TOKEN TAPM_GITEA_PACKAGE_WRITE_USERNAME TAPM_GITEA_PACKAGE_WRITE_TOKEN TAPM_COOKIE_SECRET ) for key in "${required[@]}"; do value="$(read_env_setting "$key")" if [[ -z "$value" || "$value" == replace-* ]]; then printf 'Missing broker setting: %s\n' "$key" >&2 return 1 fi done value="$(read_env_setting TAPM_COOKIE_SECRET)" if (( ${#value} < 32 )); then printf 'TAPM_COOKIE_SECRET must contain at least 32 characters.\n' >&2 return 1 fi return 0 } require_tls_certificate() { [[ -f "${SSL_CERTIFICATE_DIR}/${SSL_CERTIFICATE_FILE}" ]] || fail "TLS certificate was not found at ${SSL_CERTIFICATE_DIR}/${SSL_CERTIFICATE_FILE}; run ./manage.sh bootstrap" [[ -f "${SSL_CERTIFICATE_DIR}/${SSL_CERTIFICATE_KEY_FILE}" ]] || fail "TLS certificate key was not found at ${SSL_CERTIFICATE_DIR}/${SSL_CERTIFICATE_KEY_FILE}; run ./manage.sh bootstrap" } require_env() { [[ -f .env ]] || fail "copy .env.example to .env and configure it first" BROKER_DOMAIN="$(read_env_setting BROKER_DOMAIN)" GITEA_DOMAIN="$(read_env_setting GITEA_DOMAIN)" GITEA_PUBLIC_URL="$(read_env_setting GITEA_PUBLIC_URL)" LETSENCRYPT_EMAIL="$(read_env_setting LETSENCRYPT_EMAIL)" SSL_MODE="$(read_env_setting SSL_MODE)" SSL_MODE="${SSL_MODE:-letsencrypt}" SSL_CERTIFICATE_DIR="$(read_env_setting SSL_CERTIFICATE_DIR)" SSL_CERTIFICATE_FILE="$(read_env_setting SSL_CERTIFICATE_FILE)" SSL_CERTIFICATE_KEY_FILE="$(read_env_setting SSL_CERTIFICATE_KEY_FILE)" SSL_CERTIFICATE_FILE="${SSL_CERTIFICATE_FILE:-fullchain.pem}" SSL_CERTIFICATE_KEY_FILE="${SSL_CERTIFICATE_KEY_FILE:-privkey.pem}" : "${BROKER_DOMAIN:?BROKER_DOMAIN is required}" : "${GITEA_DOMAIN:?GITEA_DOMAIN is required}" [[ "$BROKER_DOMAIN" != "$GITEA_DOMAIN" ]] || fail "BROKER_DOMAIN and GITEA_DOMAIN must be different hostnames" if [[ -z "$GITEA_PUBLIC_URL" ]]; then if [[ "$SSL_MODE" == "none" ]]; then http_port="$(read_env_setting HTTP_PORT)" http_port="${http_port:-8680}" GITEA_PUBLIC_URL="http://${GITEA_DOMAIN}:${http_port}/" else GITEA_PUBLIC_URL="https://${GITEA_DOMAIN}/" fi fi case "$SSL_MODE" in none) [[ "$(read_env_setting TAPM_PUBLIC_URL)" == http://* ]] || fail "TAPM_PUBLIC_URL must use http:// when SSL_MODE=none" [[ "$(read_env_setting TAPM_GITEA_URL)" == http://* ]] || fail "TAPM_GITEA_URL must use http:// when SSL_MODE=none" [[ "$GITEA_PUBLIC_URL" == http://* ]] || fail "GITEA_PUBLIC_URL must use http:// when SSL_MODE=none" ;; letsencrypt) : "${LETSENCRYPT_EMAIL:?LETSENCRYPT_EMAIL is required for SSL_MODE=letsencrypt}" SSL_CERTIFICATE_DIR="${SSL_CERTIFICATE_DIR:-./config/letsencrypt/live/${BROKER_DOMAIN}}" [[ "$(read_env_setting TAPM_PUBLIC_URL)" == https://* ]] || fail "TAPM_PUBLIC_URL must use https:// when TLS is enabled" [[ "$(read_env_setting TAPM_GITEA_URL)" == https://* ]] || fail "TAPM_GITEA_URL must use https:// when TLS is enabled" [[ "$GITEA_PUBLIC_URL" == https://* ]] || fail "GITEA_PUBLIC_URL must use https:// when TLS is enabled" ;; custom) SSL_CERTIFICATE_DIR="${SSL_CERTIFICATE_DIR:-./config/ssl}" [[ -f "${SSL_CERTIFICATE_DIR}/${SSL_CERTIFICATE_FILE}" ]] || fail "custom certificate was not found at ${SSL_CERTIFICATE_DIR}/${SSL_CERTIFICATE_FILE}" [[ -f "${SSL_CERTIFICATE_DIR}/${SSL_CERTIFICATE_KEY_FILE}" ]] || fail "custom certificate key was not found at ${SSL_CERTIFICATE_DIR}/${SSL_CERTIFICATE_KEY_FILE}" [[ "$(read_env_setting TAPM_PUBLIC_URL)" == https://* ]] || fail "TAPM_PUBLIC_URL must use https:// when TLS is enabled" [[ "$(read_env_setting TAPM_GITEA_URL)" == https://* ]] || fail "TAPM_GITEA_URL must use https:// when TLS is enabled" [[ "$GITEA_PUBLIC_URL" == https://* ]] || fail "GITEA_PUBLIC_URL must use https:// when TLS is enabled" ;; *) fail "SSL_MODE must be none, letsencrypt, or custom" ;; esac export GITEA_PUBLIC_URL SSL_CERTIFICATE_DIR SSL_CERTIFICATE_FILE SSL_CERTIFICATE_KEY_FILE select_compose_stack require_stack_files } prepare() { mkdir -p \ config/broker \ config/gitea/data \ config/gitea/config \ config/letsencrypt \ config/letsencrypt/archive \ config/ssl \ config/certbot-webroot \ config/backups chmod 700 config/broker config/gitea/data config/gitea/config config/backups if [[ "$(id -u)" == 0 ]]; then chown -R 1000:1000 config/broker config/gitea fi docker network inspect tapm-edge >/dev/null 2>&1 || docker network create tapm-edge >/dev/null } case "${1:-}" in prepare) require_env prepare ;; bootstrap) require_env prepare if [[ "$SSL_MODE" == "none" ]]; then compose_stack up -d gitea nginx elif [[ "$SSL_MODE" == "letsencrypt" ]]; then docker compose up -d gitea nginx docker compose --profile tools run --rm certbot certonly \ --webroot --webroot-path /var/www/certbot \ --non-interactive --agree-tos \ --email "$LETSENCRYPT_EMAIL" \ --cert-name "$BROKER_DOMAIN" \ -d "$BROKER_DOMAIN" -d "$GITEA_DOMAIN" require_tls_certificate compose_stack up -d gitea nginx else compose_stack up -d gitea nginx fi ;; start) require_env prepare broker_config_ready || fail "broker configuration is incomplete; run ./manage.sh configure-broker" if [[ "$SSL_MODE" != "none" ]]; then require_tls_certificate fi compose_stack up -d --build ;; start-direct) require_env prepare broker_config_ready || fail "broker configuration is incomplete; run ./manage.sh configure-broker" docker compose \ "${COMPOSE_FILES[@]}" \ -f compose.direct.yaml \ up -d --build ;; create-admin) require_env command -v openssl >/dev/null 2>&1 || fail "openssl is required to generate the temporary password" printf 'Git administrator username [taiadmin]: ' read -r admin_username admin_username="${admin_username:-taiadmin}" printf 'Git administrator email: ' read -r admin_email [[ -n "$admin_email" ]] || fail "an administrator email is required" temporary_password="$(openssl rand -base64 24)" docker compose exec -T gitea gitea admin user create \ --admin \ --username "$admin_username" \ --email "$admin_email" \ --password "$temporary_password" \ --must-change-password printf '\nGit administrator created.\n' printf 'Username: %s\n' "$admin_username" printf 'Temporary password: %s\n' "$temporary_password" printf 'Sign in at %s and change the password immediately.\n' \ "$GITEA_PUBLIC_URL" ;; configure-broker) require_env command -v openssl >/dev/null 2>&1 || fail "openssl is required to generate the cookie secret" printf 'Create the Git OAuth application with this callback URL:\n' printf ' %s/auth/callback\n\n' "$(read_env_setting TAPM_PUBLIC_URL)" printf 'Git OAuth client ID: ' read -r oauth_client_id [[ -n "$oauth_client_id" ]] || fail "OAuth client ID is required" printf 'Git OAuth client secret: ' read -r -s oauth_client_secret printf '\n' [[ -n "$oauth_client_secret" ]] || fail "OAuth client secret is required" printf 'Read-only package username [tapm-packages]: ' read -r package_username package_username="${package_username:-tapm-packages}" printf 'Read-only package token: ' read -r -s package_token printf '\n' [[ -n "$package_token" ]] || fail "read-only package token is required" printf 'Package publisher username [tapm-publisher]: ' read -r publisher_username publisher_username="${publisher_username:-tapm-publisher}" printf 'Package publisher token: ' read -r -s publisher_token printf '\n' [[ -n "$publisher_token" ]] || fail "publisher token is required" printf 'Allowed Git users [taiadmin]: ' read -r allowed_users allowed_users="${allowed_users:-taiadmin}" cookie_secret="$(read_env_setting TAPM_COOKIE_SECRET)" if (( ${#cookie_secret} < 32 )); then cookie_secret="$(openssl rand -base64 48)" fi set_env_setting TAPM_GITEA_CLIENT_ID "$oauth_client_id" set_env_setting TAPM_GITEA_CLIENT_SECRET "$oauth_client_secret" set_env_setting TAPM_GITEA_PACKAGE_USERNAME "$package_username" set_env_setting TAPM_GITEA_PACKAGE_TOKEN "$package_token" set_env_setting TAPM_GITEA_PACKAGE_WRITE_USERNAME "$publisher_username" set_env_setting TAPM_GITEA_PACKAGE_WRITE_TOKEN "$publisher_token" set_env_setting TAPM_ALLOWED_GITEA_USERS "$allowed_users" set_env_setting TAPM_COOKIE_SECRET "$cookie_secret" printf 'Broker credentials saved to .env with mode 0600.\n' printf 'Start the complete stack with ./manage.sh start\n' ;; rotate-cookie-secret) require_env command -v openssl >/dev/null 2>&1 || fail "openssl is required to generate the cookie secret" set_env_setting TAPM_COOKIE_SECRET "$(openssl rand -base64 48)" printf 'TAPM_COOKIE_SECRET was rotated in .env.\n' if compose_stack ps -q broker | grep -q .; then printf 'Recreating the broker to apply it...\n' compose_stack up -d --force-recreate broker printf 'Existing technician browser sessions are now invalid.\n' fi ;; set-ssl) require_env target_mode="${2:-}" case "$target_mode" in none) http_port="$(read_env_setting HTTP_PORT)" http_port="${http_port:-8680}" set_env_setting SSL_MODE none set_env_setting TAPM_PUBLIC_URL "http://${BROKER_DOMAIN}:${http_port}" set_env_setting TAPM_GITEA_URL "http://${GITEA_DOMAIN}:${http_port}" set_env_setting GITEA_PUBLIC_URL "http://${GITEA_DOMAIN}:${http_port}/" printf 'HTTP testing mode configured. Apply it with ./manage.sh start\n' ;; letsencrypt) email="$(read_env_setting LETSENCRYPT_EMAIL)" if [[ -z "$email" ]]; then printf 'Let'\''s Encrypt account email: ' read -r email [[ -n "$email" ]] || fail "an email is required for Let's Encrypt" set_env_setting LETSENCRYPT_EMAIL "$email" fi set_env_setting SSL_MODE letsencrypt set_env_setting SSL_CERTIFICATE_DIR "./config/letsencrypt/live/${BROKER_DOMAIN}" set_env_setting TAPM_PUBLIC_URL "https://${BROKER_DOMAIN}" set_env_setting TAPM_GITEA_URL "https://${GITEA_DOMAIN}" set_env_setting GITEA_PUBLIC_URL "https://${GITEA_DOMAIN}/" printf 'TLS URLs configured. Obtain the certificate with ./manage.sh bootstrap\n' ;; custom) set_env_setting SSL_MODE custom set_env_setting SSL_CERTIFICATE_DIR ./config/ssl set_env_setting TAPM_PUBLIC_URL "https://${BROKER_DOMAIN}" set_env_setting TAPM_GITEA_URL "https://${GITEA_DOMAIN}" set_env_setting GITEA_PUBLIC_URL "https://${GITEA_DOMAIN}/" printf 'Custom TLS mode configured. Place the certificate files, then run ./manage.sh bootstrap\n' ;; *) fail "usage: ./manage.sh set-ssl {none|letsencrypt|custom}" ;; esac ;; renew) require_env [[ "$SSL_MODE" == "letsencrypt" ]] || fail "automatic renewal is only available with SSL_MODE=letsencrypt" docker compose --profile tools run --rm certbot renew --quiet compose_stack exec nginx nginx -s reload ;; backup) require_env prepare stamp="$(date -u +%Y%m%dT%H%M%SZ)" destination="config/backups/${stamp}" mkdir -p "$destination" docker compose stop broker docker compose stop gitea cp -a config/broker "$destination/" cp -a config/gitea "$destination/" compose_stack start gitea broker printf 'Backup created at %s\n' "$destination" ;; status) require_env compose_stack ps ;; diagnostics) require_env printf '%s\n' 'Compose services:' compose_stack config --services printf '\n%s\n' 'Container status:' compose_stack ps printf '\n%s\n' 'Recent Nginx logs:' compose_stack logs --tail=100 nginx printf '\n%s\n' 'Published Nginx ports:' compose_stack port nginx 80 || true if [[ "$SSL_MODE" != "none" ]]; then compose_stack port nginx 443 || true fi ;; broker-container-id) require_env compose_stack ps -q broker ;; broker-logs) require_env compose_stack logs --tail=100 broker ;; *) printf 'Usage: %s {prepare|bootstrap|configure-broker|rotate-cookie-secret|set-ssl|start|start-direct|create-admin|renew|backup|status|diagnostics}\n' "$0" exit 2 ;; esac