381 lines
15 KiB
Bash
Executable File
381 lines
15 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
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Generate compose file for a single space
|
|
# ---------------------------------------------------------------------------
|
|
generate_space() {
|
|
local space="$1"
|
|
|
|
local primary_domain fallback_domain email_from slug infisical_slug
|
|
primary_domain=$(get_space "$space" "primary_domain")
|
|
fallback_domain=$(get_space "$space" "fallback_domain")
|
|
email_from=$(get_space "$space" "email_from")
|
|
slug=$(get_space "$space" "slug")
|
|
infisical_slug=$(get_space "$space" "infisical_slug")
|
|
|
|
if [[ -z "$primary_domain" || "$primary_domain" == "null" ]]; then
|
|
echo -e "${RED} Error: spaces.${space}.primary_domain is required${NC}"
|
|
return 1
|
|
fi
|
|
|
|
# Slug defaults to space key name
|
|
if [[ -z "$slug" || "$slug" == "null" ]]; then
|
|
slug="$space"
|
|
fi
|
|
|
|
# Infisical slug defaults to postiz-<space>
|
|
if [[ -z "$infisical_slug" || "$infisical_slug" == "null" ]]; then
|
|
infisical_slug="postiz-${space}"
|
|
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
|
|
local use_sablier use_oauth
|
|
local oauth_url oauth_auth_url oauth_token_url oauth_userinfo_url
|
|
local oauth_display_name oauth_logo_url
|
|
|
|
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")
|
|
use_sablier=$(get_space_override "$space" "sablier")
|
|
use_oauth=$(get_space_override "$space" "oauth")
|
|
oauth_url=$(get_space_override "$space" "oauth_url")
|
|
oauth_auth_url=$(get_space_override "$space" "oauth_auth_url")
|
|
oauth_token_url=$(get_space_override "$space" "oauth_token_url")
|
|
oauth_userinfo_url=$(get_space_override "$space" "oauth_userinfo_url")
|
|
oauth_display_name=$(get_space_override "$space" "oauth_display_name")
|
|
oauth_logo_url=$(get_space_override "$space" "oauth_logo_url")
|
|
|
|
# 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
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Build dynamic blocks
|
|
# -----------------------------------------------------------------------
|
|
|
|
# OAuth block (config only — client_id/secret injected by Infisical)
|
|
local oauth_block
|
|
if [[ "$use_oauth" == "true" ]]; then
|
|
oauth_block=" POSTIZ_GENERIC_OAUTH: 'true'
|
|
NEXT_PUBLIC_POSTIZ_OAUTH_DISPLAY_NAME: '${oauth_display_name}'
|
|
NEXT_PUBLIC_POSTIZ_OAUTH_LOGO_URL: '${oauth_logo_url}'
|
|
POSTIZ_OAUTH_URL: '${oauth_url}'
|
|
POSTIZ_OAUTH_AUTH_URL: '${oauth_auth_url}'
|
|
POSTIZ_OAUTH_TOKEN_URL: '${oauth_token_url}'
|
|
POSTIZ_OAUTH_USERINFO_URL: '${oauth_userinfo_url}'
|
|
POSTIZ_OAUTH_SCOPE: 'openid profile email'
|
|
# POSTIZ_OAUTH_CLIENT_ID + CLIENT_SECRET from Infisical"
|
|
else
|
|
oauth_block=" # OAuth disabled for this space"
|
|
fi
|
|
|
|
# Traefik + Sablier labels
|
|
local traefik_labels sablier_labels_db sablier_labels_redis
|
|
if [[ "$use_sablier" == "true" ]]; then
|
|
traefik_labels=" - \"traefik.enable=false\"
|
|
- \"sablier.enable=true\"
|
|
- \"sablier.group=postiz-${slug}\"
|
|
- \"traefik.http.routers.postiz-${slug}.rule=Host(\`${primary_domain}\`) || Host(\`${fallback_domain}\`)\"
|
|
- \"traefik.http.routers.postiz-${slug}.entrypoints=web,websecure\"
|
|
- \"traefik.http.services.postiz-${slug}.loadbalancer.server.port=${postiz_port}\""
|
|
sablier_labels_db=" labels:
|
|
- \"sablier.enable=true\"
|
|
- \"sablier.group=postiz-${slug}\""
|
|
sablier_labels_redis=" labels:
|
|
- \"sablier.enable=true\"
|
|
- \"sablier.group=postiz-${slug}\""
|
|
else
|
|
traefik_labels=" - \"traefik.enable=true\"
|
|
- \"traefik.http.routers.postiz-${slug}.rule=Host(\`${primary_domain}\`) || Host(\`${fallback_domain}\`)\"
|
|
- \"traefik.http.routers.postiz-${slug}.entrypoints=web,websecure\"
|
|
- \"traefik.http.services.postiz-${slug}.loadbalancer.server.port=${postiz_port}\"
|
|
- \"traefik.docker.network=traefik-public\""
|
|
sablier_labels_db=""
|
|
sablier_labels_redis=""
|
|
fi
|
|
|
|
local outfile="${OUTDIR}/docker-compose.space-${space}.yml"
|
|
|
|
# Perform template substitution using sed
|
|
sed \
|
|
-e "s|{{SPACE_NAME}}|${space}|g" \
|
|
-e "s|{{SPACE_SLUG}}|${slug}|g" \
|
|
-e "s|{{PRIMARY_DOMAIN}}|${primary_domain}|g" \
|
|
-e "s|{{FALLBACK_DOMAIN}}|${fallback_domain}|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" \
|
|
-e "s|{{INFISICAL_SLUG}}|${infisical_slug}|g" \
|
|
"$TEMPLATE" > "$outfile"
|
|
|
|
# Replace multi-line blocks (sed can't do these well, use temp files)
|
|
# OAuth block
|
|
local tmpfile
|
|
tmpfile=$(mktemp)
|
|
awk -v block="$oauth_block" '{gsub(/{{OAUTH_BLOCK}}/, block); print}' "$outfile" > "$tmpfile"
|
|
mv "$tmpfile" "$outfile"
|
|
|
|
# Traefik labels block
|
|
tmpfile=$(mktemp)
|
|
awk -v block="$traefik_labels" '{gsub(/{{TRAEFIK_LABELS}}/, block); print}' "$outfile" > "$tmpfile"
|
|
mv "$tmpfile" "$outfile"
|
|
|
|
# Sablier labels for DB
|
|
tmpfile=$(mktemp)
|
|
awk -v block="$sablier_labels_db" '{gsub(/{{SABLIER_LABELS_DB}}/, block); print}' "$outfile" > "$tmpfile"
|
|
mv "$tmpfile" "$outfile"
|
|
|
|
# Sablier labels for Redis
|
|
tmpfile=$(mktemp)
|
|
awk -v block="$sablier_labels_redis" '{gsub(/{{SABLIER_LABELS_REDIS}}/, block); print}' "$outfile" > "$tmpfile"
|
|
mv "$tmpfile" "$outfile"
|
|
|
|
echo -e "${GREEN} ✓ ${outfile}${NC}"
|
|
|
|
# Generate minimal .env template (only Infisical creds + postgres password)
|
|
local envfile="${OUTDIR}/env.space-${space}"
|
|
cat > "$envfile" <<EOF
|
|
# Minimal secrets for ${space} — all other secrets from Infisical
|
|
# Copy to server alongside docker-compose.space-${space}.yml
|
|
|
|
# Infisical Machine Identity (shared across spaces)
|
|
INFISICAL_CLIENT_ID=
|
|
INFISICAL_CLIENT_SECRET=
|
|
|
|
# Postgres password (needed by postgres container directly)
|
|
POSTGRES_PASSWORD=
|
|
EOF
|
|
echo -e "${GREEN} ✓ ${envfile}${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]}"
|
|
|
|
cat >> "$dns_file" <<EOF
|
|
# Space: ${ALL_SPACE_NAMES[$i]}
|
|
echo "Adding DNS for ${domain}..."
|
|
# Add CNAME: ${domain} -> ${tunnel_cname}
|
|
# (Use Cloudflare dashboard or API - zone ID needed per domain)
|
|
|
|
echo "Adding DNS for ${fallback}..."
|
|
# Add CNAME: ${fallback} -> ${tunnel_cname}
|
|
# (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 "$@"
|