#!/usr/bin/env bash set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd "${script_dir}/.." && pwd)" global_caddy_dir="/srv/global-caddy" container_name="global_caddy" upstream_host="host.docker.internal" bootstrap_mode="auto" public_https_port="443" instance_dirs=() usage() { cat <<'USAGE' Usage: ./scripts/setup-shared-host-global-caddy.sh [options] Configures one shared/global Caddy container for multiple repo-based cloud instances running the no-SSL stack on the same host. Options: --instance-dir Cloud repo directory to configure (repeatable) --global-caddy-dir Shared Caddy config dir (default: /srv/global-caddy) --container-name Shared Caddy container name (default: global_caddy) --upstream-host Upstream host for global Caddy (default: host.docker.internal) --public-https-port Public HTTPS port served by shared Caddy (default: 443) --bootstrap Force bootstrap/start behavior for the shared Caddy target --no-bootstrap Assume shared Caddy already exists -h, --help Show this help If no --instance-dir values are supplied, the script prompts for them interactively. USAGE } resolve_path() { local raw_path="$1" if [[ "${raw_path}" == /* ]]; then echo "${raw_path}" else echo "${PWD}/${raw_path}" fi } read_kv() { local env_file="$1" local key="$2" local line line="$(grep -m1 -E "^${key}=" "${env_file}" || true)" if [[ -z "${line}" ]]; then return 1 fi echo "${line#*=}" } set_kv() { local env_file="$1" local key="$2" local value="$3" local esc esc="${value//\\/\\\\}" esc="${esc//&/\\&}" esc="${esc//|/\\|}" if grep -q -E "^${key}=" "${env_file}"; then sed -i -E "s|^${key}=.*|${key}=${esc}|" "${env_file}" else printf '\n%s=%s\n' "${key}" "${value}" >> "${env_file}" fi } trim_csv() { local raw="${1:-}" local cleaned cleaned="$(echo "${raw}" | tr -d '[:space:]')" cleaned="${cleaned#,}" cleaned="${cleaned%,}" echo "${cleaned}" } profile_enabled() { local profiles="$1" local name="$2" [[ ",${profiles}," == *",${name},"* ]] } prompt_instance_dirs() { local answer="" echo "Enter cloud repo directories one per line. Leave blank when finished." while true; do read -r -p "Instance repo directory: " answer || true answer="${answer//[$'\r\n']}" if [[ -z "${answer}" ]]; then break fi instance_dirs+=("$(resolve_path "${answer}")") done } run_in_repo() { local repo_dir="$1" shift (cd "${repo_dir}" && "$@") } configure_instance_env() { local repo_dir="$1" local env_file="${repo_dir}/.env.devprod" local profiles gateway_url profiles="$(trim_csv "$(read_kv "${env_file}" "COMPOSE_PROFILES" || true)")" set_kv "${env_file}" "EXTERNAL_PROXY_MODE" "global-caddy" set_kv "${env_file}" "PUBLIC_HTTPS_PORT" "${public_https_port}" set_kv "${env_file}" "DEVPROD_HTTP_BIND_HOST" "0.0.0.0" set_kv "${env_file}" "DEVPROD_BROKER_BIND_HOST" "0.0.0.0" if profile_enabled "${profiles}" "office"; then set_kv "${env_file}" "COLLABORA_BIND_HOST" "0.0.0.0" fi if profile_enabled "${profiles}" "recording"; then set_kv "${env_file}" "TALK_RECORDING_BIND_HOST" "0.0.0.0" fi gateway_url="$(read_kv "${env_file}" "QORTAL_PUBLIC_GATEWAY_URL" || true)" if [[ -z "${gateway_url}" ]]; then gateway_url="$(read_kv "${env_file}" "QORTAL_GATEWAY_PUBLIC_URL" || true)" fi if [[ -z "${gateway_url}" ]]; then gateway_url="$(read_kv "${env_file}" "QORTAL_GATEWAY_URL" || true)" fi if [[ -z "${gateway_url}" && -t 0 ]]; then read -r -p "Optional public Qortal gateway URL for ${repo_dir} (blank to skip): " gateway_url || true gateway_url="${gateway_url//[$'\r\n']}" if [[ -n "${gateway_url}" ]]; then set_kv "${env_file}" "QORTAL_PUBLIC_GATEWAY_URL" "${gateway_url}" fi fi if [[ -n "${gateway_url}" ]]; then set_kv "${env_file}" "QORTAL_NODE_GATEWAY_BIND_HOST" "0.0.0.0" fi } ensure_instance_layout() { local repo_dir="$1" local required=( ".env.devprod" "stop-devprod.sh" "recreate-devprod.sh" "scripts/register-global-caddy-proxy.sh" "scripts/ensure-nextcloud-url-config.sh" ) local path if [[ ! -d "${repo_dir}" ]]; then echo "Instance repo directory not found: ${repo_dir}" exit 1 fi for path in "${required[@]}"; do if [[ ! -e "${repo_dir}/${path}" ]]; then echo "Missing required file in ${repo_dir}: ${path}" exit 1 fi done } while [[ $# -gt 0 ]]; do case "$1" in --instance-dir) instance_dirs+=("$(resolve_path "${2:-}")") shift 2 ;; --instance-dir=*) instance_dirs+=("$(resolve_path "${1#*=}")") shift ;; --global-caddy-dir) global_caddy_dir="$(resolve_path "${2:-}")" shift 2 ;; --global-caddy-dir=*) global_caddy_dir="$(resolve_path "${1#*=}")" shift ;; --container-name) container_name="${2:-}" shift 2 ;; --container-name=*) container_name="${1#*=}" shift ;; --upstream-host) upstream_host="${2:-}" shift 2 ;; --upstream-host=*) upstream_host="${1#*=}" shift ;; --public-https-port) public_https_port="${2:-}" shift 2 ;; --public-https-port=*) public_https_port="${1#*=}" shift ;; --bootstrap) bootstrap_mode="1" shift ;; --no-bootstrap) bootstrap_mode="0" shift ;; -h|--help) usage exit 0 ;; *) echo "Unknown option: $1" usage exit 1 ;; esac done if [[ "${#instance_dirs[@]}" -eq 0 ]]; then if [[ -t 0 ]]; then prompt_instance_dirs fi fi if [[ "${#instance_dirs[@]}" -eq 0 ]]; then echo "No instance directories provided." usage exit 1 fi if [[ "${bootstrap_mode}" == "auto" ]]; then if [[ -f "${global_caddy_dir}/docker-compose.yml" ]]; then bootstrap_mode="0" else bootstrap_mode="1" fi fi echo "Shared Caddy target: ${global_caddy_dir}" echo "Shared Caddy container: ${container_name}" echo "Upstream host: ${upstream_host}" echo "Public HTTPS port: ${public_https_port}" echo "Bootstrap shared Caddy: ${bootstrap_mode}" echo for repo_dir in "${instance_dirs[@]}"; do ensure_instance_layout "${repo_dir}" echo "Configuring instance: ${repo_dir}" configure_instance_env "${repo_dir}" echo " Stopping bundled SSL stack if present..." run_in_repo "${repo_dir}" ./stop-devprod.sh --ssl || true echo " Recreating no-SSL stack..." run_in_repo "${repo_dir}" ./recreate-devprod.sh --nossl echo " Registering instance in shared Caddy..." if [[ "${bootstrap_mode}" == "1" ]]; then run_in_repo "${repo_dir}" ./scripts/register-global-caddy-proxy.sh \ --env-file .env.devprod \ --target-dir "${global_caddy_dir}" \ --upstream-host "${upstream_host}" \ --bootstrap \ --start \ --reload \ --container-name "${container_name}" bootstrap_mode="0" else run_in_repo "${repo_dir}" ./scripts/register-global-caddy-proxy.sh \ --env-file .env.devprod \ --target-dir "${global_caddy_dir}" \ --upstream-host "${upstream_host}" \ --reload \ --container-name "${container_name}" fi echo " Syncing Nextcloud URL/proxy config..." run_in_repo "${repo_dir}" ./scripts/ensure-nextcloud-url-config.sh \ --compose-file docker-compose.devprod.nossl.yml \ --env-file .env.devprod if [[ -x "${repo_dir}/scripts/ensure-qortal-integration-runtime-config.sh" ]]; then echo " Syncing qortal_integration runtime config..." run_in_repo "${repo_dir}" ./scripts/ensure-qortal-integration-runtime-config.sh \ --compose-file docker-compose.devprod.nossl.yml \ --env-file .env.devprod fi if [[ -x "${repo_dir}/scripts/ensure-nextcloud-office-config.sh" ]]; then echo " Validating Office/Collabora configuration..." run_in_repo "${repo_dir}" ./scripts/ensure-nextcloud-office-config.sh \ --compose-file docker-compose.devprod.nossl.yml \ --env-file .env.devprod fi if [[ -x "${repo_dir}/scripts/ensure-nextcloud-talk-config.sh" ]]; then echo " Validating Talk HPB/recording configuration..." run_in_repo "${repo_dir}" ./scripts/ensure-nextcloud-talk-config.sh \ --compose-file docker-compose.devprod.nossl.yml \ --env-file .env.devprod fi echo " Completed: ${repo_dir}" echo done echo "Shared-host global Caddy setup complete."