Files
TA-Deployment-Broker/manage.sh
T
2026-07-28 18:01:29 -05:00

415 lines
13 KiB
Bash
Executable File

#!/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_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/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"
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"
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 'Gitea administrator username [taiadmin]: '
read -r admin_username
admin_username="${admin_username:-taiadmin}"
printf 'Gitea 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 '\nGitea 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 Gitea OAuth application with this callback URL:\n'
printf ' %s/auth/callback\n\n' "$(read_env_setting TAPM_PUBLIC_URL)"
printf 'Gitea OAuth client ID: '
read -r oauth_client_id
[[ -n "$oauth_client_id" ]] || fail "OAuth client ID is required"
printf 'Gitea 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 Gitea 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