#!/usr/bin/env bash # ============================================================================= # generate.sh — Generate per-space docker-compose files from spaces.yml # ============================================================================= # # Reads spaces.yml and produces: # generated/docker-compose.space-.yml (per-space Postiz stack) # generated/tunnel-hostnames.yml (Cloudflare tunnel entries) # generated/dns-commands.sh (Cloudflare DNS CNAME commands) # # Requirements: yq (https://github.com/mikefarah/yq) v4+ # # Usage: # ./generate.sh # Generate all spaces # ./generate.sh crypto-commons # Generate a single space # set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONFIG="${SCRIPT_DIR}/spaces.yml" TEMPLATE="${SCRIPT_DIR}/docker-compose.template.yml" OUTDIR="${SCRIPT_DIR}/generated" # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' # No Color # --------------------------------------------------------------------------- # Preflight checks # --------------------------------------------------------------------------- if ! command -v yq &>/dev/null; then echo -e "${RED}Error: yq is required but not installed.${NC}" echo " Install: https://github.com/mikefarah/yq#install" echo " Quick: sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 && sudo chmod +x /usr/local/bin/yq" exit 1 fi if [[ ! -f "$CONFIG" ]]; then echo -e "${RED}Error: spaces.yml not found at ${CONFIG}${NC}" exit 1 fi if [[ ! -f "$TEMPLATE" ]]; then echo -e "${RED}Error: docker-compose.template.yml not found at ${TEMPLATE}${NC}" exit 1 fi # --------------------------------------------------------------------------- # Read defaults from spaces.yml # --------------------------------------------------------------------------- get_default() { local key="$1" yq ".defaults.postiz.${key}" "$CONFIG" 2>/dev/null || echo "" } get_space() { local space="$1" key="$2" yq ".spaces.${space}.${key}" "$CONFIG" 2>/dev/null || echo "" } get_space_override() { local space="$1" key="$2" local val val=$(yq ".spaces.${space}.postiz.${key} // \"\"" "$CONFIG" 2>/dev/null) if [[ -z "$val" || "$val" == "null" ]]; then get_default "$key" else echo "$val" fi } # Escape dots for regex (used in Traefik redirect rules) escape_dots() { # Produce \\. for each dot (double-escaped for YAML double-quoted strings) # sed replacement needs 4 backslashes to produce 2 in output echo "$1" | sed 's/\./\\\\\\\\./g' } # --------------------------------------------------------------------------- # Generate compose file for a single space # --------------------------------------------------------------------------- generate_space() { local space="$1" local primary_domain fallback_domain email_from primary_domain=$(get_space "$space" "primary_domain") fallback_domain=$(get_space "$space" "fallback_domain") email_from=$(get_space "$space" "email_from") if [[ -z "$primary_domain" || "$primary_domain" == "null" ]]; then echo -e "${RED} Error: spaces.${space}.primary_domain is required${NC}" return 1 fi # Read defaults (with per-space overrides) local postiz_image postiz_port postgres_image redis_image temporal_image temporal_pg_image local email_provider email_from_name email_host email_port email_secure email_user local storage_provider upload_dir disable_reg is_general api_limit postiz_image=$(get_space_override "$space" "image") postiz_port=$(get_space_override "$space" "port") postgres_image=$(get_space_override "$space" "postgres_image") redis_image=$(get_space_override "$space" "redis_image") temporal_image=$(get_space_override "$space" "temporal_image") temporal_pg_image=$(get_space_override "$space" "temporal_postgres_image") email_provider=$(get_space_override "$space" "email_provider") email_from_name=$(get_space_override "$space" "email_from_name") email_host=$(get_space_override "$space" "email_host") email_port=$(get_space_override "$space" "email_port") email_secure=$(get_space_override "$space" "email_secure") email_user=$(get_space_override "$space" "email_user") storage_provider=$(get_space_override "$space" "storage_provider") upload_dir=$(get_space_override "$space" "upload_directory") disable_reg=$(get_space_override "$space" "disable_registration") is_general=$(get_space_override "$space" "is_general") api_limit=$(get_space_override "$space" "api_limit") # Fallback domain defaults if [[ -z "$fallback_domain" || "$fallback_domain" == "null" ]]; then fallback_domain="${space}.rsocials.online" fi if [[ -z "$email_from" || "$email_from" == "null" ]]; then email_from="$email_user" fi local fallback_escaped fallback_escaped=$(escape_dots "$fallback_domain") local outfile="${OUTDIR}/docker-compose.space-${space}.yml" # Perform template substitution sed \ -e "s|{{SPACE_NAME}}|${space}|g" \ -e "s|{{SPACE_SLUG}}|${space}|g" \ -e "s|{{PRIMARY_DOMAIN}}|${primary_domain}|g" \ -e "s|{{FALLBACK_DOMAIN}}|${fallback_domain}|g" \ -e "s|{{FALLBACK_ESCAPED}}|${fallback_escaped}|g" \ -e "s|{{POSTIZ_IMAGE}}|${postiz_image}|g" \ -e "s|{{POSTIZ_PORT}}|${postiz_port}|g" \ -e "s|{{POSTGRES_IMAGE}}|${postgres_image}|g" \ -e "s|{{REDIS_IMAGE}}|${redis_image}|g" \ -e "s|{{TEMPORAL_IMAGE}}|${temporal_image}|g" \ -e "s|{{TEMPORAL_PG_IMAGE}}|${temporal_pg_image}|g" \ -e "s|{{EMAIL_PROVIDER}}|${email_provider}|g" \ -e "s|{{EMAIL_FROM_NAME}}|${email_from_name}|g" \ -e "s|{{EMAIL_FROM}}|${email_from}|g" \ -e "s|{{EMAIL_HOST}}|${email_host}|g" \ -e "s|{{EMAIL_PORT}}|${email_port}|g" \ -e "s|{{EMAIL_SECURE}}|${email_secure}|g" \ -e "s|{{EMAIL_USER}}|${email_user}|g" \ -e "s|{{STORAGE_PROVIDER}}|${storage_provider}|g" \ -e "s|{{UPLOAD_DIR}}|${upload_dir}|g" \ -e "s|{{DISABLE_REG}}|${disable_reg}|g" \ -e "s|{{IS_GENERAL}}|${is_general}|g" \ -e "s|{{API_LIMIT}}|${api_limit}|g" \ "$TEMPLATE" > "$outfile" echo -e "${GREEN} ✓ ${outfile}${NC}" # Collect domains for tunnel/dns generation ALL_PRIMARY_DOMAINS+=("$primary_domain") ALL_FALLBACK_DOMAINS+=("$fallback_domain") ALL_SPACE_NAMES+=("$space") } # --------------------------------------------------------------------------- # Generate Cloudflare tunnel hostname entries # --------------------------------------------------------------------------- generate_tunnel_config() { local tunnel_cname tunnel_cname=$(yq '.cloudflare.tunnel_cname' "$CONFIG" 2>/dev/null) local tunnel_file="${OUTDIR}/tunnel-hostnames.yml" cat > "$tunnel_file" <<'HEADER' # Cloudflare Tunnel Hostname Entries # Add these to /root/cloudflared/config.yml on Netcup # Then restart: docker restart cloudflared HEADER for i in "${!ALL_PRIMARY_DOMAINS[@]}"; do cat >> "$tunnel_file" < "$dns_file" <
> "$dns_file" < tunnel # (Use Cloudflare dashboard or API - zone ID needed per domain) echo "Adding DNS for ${fallback}..." # Add CNAME: ${fallback_name}.${fallback_zone} -> tunnel # (Use Cloudflare dashboard or API - zone ID needed per domain) EOF done chmod +x "$dns_file" echo -e "${GREEN} ✓ ${dns_file}${NC}" } # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- main() { echo -e "${CYAN}╔═══════════════════════════════════════════╗${NC}" echo -e "${CYAN}║ rSpace Config Generator ║${NC}" echo -e "${CYAN}╚═══════════════════════════════════════════╝${NC}" echo mkdir -p "$OUTDIR" # Collect all domains across spaces declare -a ALL_PRIMARY_DOMAINS=() declare -a ALL_FALLBACK_DOMAINS=() declare -a ALL_SPACE_NAMES=() local filter_space="${1:-}" # Get list of spaces local spaces spaces=$(yq '.spaces | keys | .[]' "$CONFIG" 2>/dev/null) if [[ -z "$spaces" ]]; then echo -e "${RED}No spaces defined in spaces.yml${NC}" exit 1 fi echo -e "${YELLOW}Generating compose files...${NC}" for space in $spaces; do if [[ -n "$filter_space" && "$space" != "$filter_space" ]]; then continue fi echo -e " ${CYAN}Space: ${space}${NC}" generate_space "$space" done echo echo -e "${YELLOW}Generating tunnel & DNS config...${NC}" generate_tunnel_config echo echo -e "${GREEN}═══════════════════════════════════════════${NC}" echo -e "${GREEN}Done! Generated files in: ${OUTDIR}/${NC}" echo -e "${GREEN}═══════════════════════════════════════════${NC}" echo echo -e "Next steps:" echo -e " 1. Review generated files in ${OUTDIR}/" echo -e " 2. Copy compose files to server or use directly" echo -e " 3. Add secrets (.env or Infisical) for each space" echo -e " 4. Deploy: ${CYAN}docker compose -f generated/docker-compose.space-.yml up -d${NC}" echo -e " 5. Update Cloudflare tunnel config (see ${OUTDIR}/tunnel-hostnames.yml)" echo -e " 6. Add DNS CNAMEs (see ${OUTDIR}/dns-commands.sh)" } main "$@"