298 lines
11 KiB
Bash
Executable File
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 "$@"
|