rsocials-online/generate.sh

298 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# =============================================================================
# generate.sh — Generate per-space docker-compose files from spaces.yml
# =============================================================================
#
# Reads spaces.yml and produces:
# generated/docker-compose.space-<name>.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" <<EOF
# Space: ${ALL_SPACE_NAMES[$i]}
- hostname: ${ALL_PRIMARY_DOMAINS[$i]}
service: http://localhost:80
- hostname: ${ALL_FALLBACK_DOMAINS[$i]}
service: http://localhost:80
EOF
done
echo -e "${GREEN}${tunnel_file}${NC}"
# DNS commands
local dns_file="${OUTDIR}/dns-commands.sh"
cat > "$dns_file" <<HEADER
#!/usr/bin/env bash
# Cloudflare DNS CNAME commands for all spaces
# Run these from Netcup (or anywhere with CF API access)
# Requires: \$CLOUDFLARE_ZONE_TOKEN set in environment
#
# Tunnel CNAME target: ${tunnel_cname}
HEADER
for i in "${!ALL_PRIMARY_DOMAINS[@]}"; do
local domain="${ALL_PRIMARY_DOMAINS[$i]}"
local fallback="${ALL_FALLBACK_DOMAINS[$i]}"
# Extract zone (last two parts of domain)
local primary_zone
primary_zone=$(echo "$domain" | rev | cut -d. -f1-2 | rev)
local fallback_zone
fallback_zone=$(echo "$fallback" | rev | cut -d. -f1-2 | rev)
# Extract record name (everything before the zone)
local primary_name
primary_name=$(echo "$domain" | sed "s/\\.${primary_zone}$//")
local fallback_name
fallback_name=$(echo "$fallback" | sed "s/\\.${fallback_zone}$//")
cat >> "$dns_file" <<EOF
# Space: ${ALL_SPACE_NAMES[$i]}
echo "Adding DNS for ${domain}..."
# Add CNAME: ${primary_name}.${primary_zone} -> 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-<name>.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 "$@"