658 lines
23 KiB
Bash
658 lines
23 KiB
Bash
#!/usr/bin/env bash
|
|
#===============================================================================
|
|
# generate-customer.sh — Generate customer-specific Portainer templates
|
|
#
|
|
# Creates a customer directory with:
|
|
# - templates.json Portainer App Templates (secrets as defaults, zero-touch deploy)
|
|
# - <app>/docker-compose.yml Compose files with domain + secrets baked in
|
|
# - secrets.env Reference file with all generated secrets
|
|
#
|
|
# Run from the app-catalog-felhom.eu repo root.
|
|
#
|
|
# Usage:
|
|
# ./scripts/generate-customer.sh --customer demo-felhom --domain demo-felhom.eu \
|
|
# --hdd-path /mnt/hdd_1 --push
|
|
#
|
|
# Version: 1.0.0
|
|
#===============================================================================
|
|
set -euo pipefail
|
|
|
|
SCRIPT_VERSION="1.0.0"
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Configuration
|
|
#-------------------------------------------------------------------------------
|
|
CUSTOMERS_REPO_GIT="https://gitea.dooplex.hu/admin/customers-felhom.eu.git"
|
|
CUSTOMERS_REPO_RAW="https://gitea.dooplex.hu/admin/customers-felhom.eu"
|
|
OUTPUT_BASE="./output"
|
|
|
|
# Defaults
|
|
CUSTOMER_ID=""
|
|
DOMAIN=""
|
|
HDD_PATH=""
|
|
APP_LIST="" # comma-separated, or empty for all
|
|
PUSH=false
|
|
DRY_RUN=false
|
|
DEBUG_MODE=false
|
|
|
|
# Apps that require HDD_PATH in compose volumes
|
|
APPS_NEEDING_HDD="filebrowser immich paperless-ngx romm"
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# App secret definitions
|
|
#
|
|
# Format: "app_name:VAR_NAME:type:param"
|
|
#
|
|
# Types:
|
|
# password:LENGTH — alphanumeric random string (a-zA-Z0-9)
|
|
# hex:LENGTH — hex string (for crypto secrets)
|
|
# static:VALUE — fixed value
|
|
#
|
|
# To add secrets for a new app, just add lines here.
|
|
#-------------------------------------------------------------------------------
|
|
APP_SECRET_DEFS=(
|
|
# Docmost
|
|
"docmost:APP_SECRET:hex:32"
|
|
"docmost:DB_PASSWORD:password:24"
|
|
|
|
# Immich
|
|
"immich:DB_PASSWORD:password:24"
|
|
|
|
# Paperless-ngx
|
|
"paperless-ngx:PAPERLESS_SECRET_KEY:hex:32"
|
|
"paperless-ngx:DB_PASSWORD:password:24"
|
|
"paperless-ngx:PAPERLESS_ADMIN_USER:static:admin"
|
|
"paperless-ngx:PAPERLESS_ADMIN_PASSWORD:password:16"
|
|
|
|
# ROMM
|
|
"romm:DB_PASSWORD:password:24"
|
|
"romm:MYSQL_ROOT_PASSWORD:password:24"
|
|
"romm:ROMM_AUTH_SECRET_KEY:hex:32"
|
|
|
|
# Vaultwarden
|
|
"vaultwarden:ADMIN_TOKEN:hex:32"
|
|
"vaultwarden:SIGNUPS_ALLOWED:static:true"
|
|
)
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Colors and logging
|
|
#-------------------------------------------------------------------------------
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
BOLD='\033[1m'
|
|
NC='\033[0m'
|
|
|
|
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|
log_step() { echo -e "${BLUE}[STEP]${NC} $1"; }
|
|
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
|
|
log_debug() { [[ "$DEBUG_MODE" == true ]] && echo -e "${CYAN}[DEBUG]${NC} $1" || true; }
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Help
|
|
#-------------------------------------------------------------------------------
|
|
print_help() {
|
|
cat << 'EOF'
|
|
generate-customer.sh — Generate customer-specific Portainer templates
|
|
|
|
Creates a customer directory with baked-in secrets and domain config,
|
|
ready for zero-touch deployment via Portainer App Templates.
|
|
|
|
USAGE:
|
|
./scripts/generate-customer.sh [OPTIONS]
|
|
|
|
REQUIRED:
|
|
--customer ID Customer identifier (e.g., demo-felhom)
|
|
--domain DOMAIN Customer domain (e.g., demo-felhom.eu)
|
|
|
|
OPTIONAL:
|
|
--hdd-path PATH External HDD mount path (e.g., /mnt/hdd_1)
|
|
If omitted, apps needing HDD show a field in Portainer.
|
|
--apps LIST Comma-separated app list (default: all available)
|
|
Available: actualbudget,docmost,filebrowser,homebox,
|
|
immich,mealie,paperless-ngx,romm,
|
|
stirling-pdf,vaultwarden
|
|
--push Git commit and push to customers-felhom.eu repo
|
|
--dry-run Show what would be done, create nothing
|
|
--debug Verbose output
|
|
-h, --help Show this help
|
|
|
|
OUTPUT:
|
|
./output/<customer>/
|
|
├── templates.json Portainer App Templates (point --templates here)
|
|
├── secrets.env All generated secrets (reference)
|
|
├── actualbudget/docker-compose.yml
|
|
├── docmost/docker-compose.yml
|
|
└── ...
|
|
|
|
Portainer templates URL:
|
|
https://gitea.dooplex.hu/admin/customers-felhom.eu/raw/branch/main/<customer>/templates.json
|
|
|
|
EXAMPLES:
|
|
# All apps, with HDD
|
|
./scripts/generate-customer.sh --customer demo-felhom \
|
|
--domain demo-felhom.eu --hdd-path /mnt/hdd_1 --push
|
|
|
|
# Raspberry Pi — lightweight apps only
|
|
./scripts/generate-customer.sh --customer pi-customer-1 \
|
|
--domain pi-customer-1.local \
|
|
--apps actualbudget,filebrowser,mealie,stirling-pdf,vaultwarden
|
|
|
|
# Preview without creating files
|
|
./scripts/generate-customer.sh --customer test --domain test.local --dry-run
|
|
EOF
|
|
}
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Argument parsing
|
|
#-------------------------------------------------------------------------------
|
|
parse_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--customer)
|
|
[[ $# -lt 2 || "$2" == --* ]] && { log_error "--customer requires a value"; exit 1; }
|
|
CUSTOMER_ID="$2"; shift 2 ;;
|
|
--domain)
|
|
[[ $# -lt 2 || "$2" == --* ]] && { log_error "--domain requires a value"; exit 1; }
|
|
DOMAIN="$2"; shift 2 ;;
|
|
--hdd-path)
|
|
[[ $# -lt 2 || "$2" == --* ]] && { log_error "--hdd-path requires a value"; exit 1; }
|
|
HDD_PATH="${2%/}"; shift 2 ;; # strip trailing slash
|
|
--apps)
|
|
[[ $# -lt 2 || "$2" == --* ]] && { log_error "--apps requires a value"; exit 1; }
|
|
APP_LIST="$2"; shift 2 ;;
|
|
--push) PUSH=true; shift ;;
|
|
--dry-run) DRY_RUN=true; shift ;;
|
|
--debug) DEBUG_MODE=true; shift ;;
|
|
-h|--help) print_help; exit 0 ;;
|
|
*) log_error "Unknown option: $1"; print_help; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
# Validate required args
|
|
if [[ -z "$CUSTOMER_ID" ]]; then
|
|
log_error "Missing required: --customer"
|
|
exit 1
|
|
fi
|
|
if [[ -z "$DOMAIN" ]]; then
|
|
log_error "Missing required: --domain"
|
|
exit 1
|
|
fi
|
|
|
|
# Sanitize customer ID (alphanumeric, hyphens, underscores only)
|
|
if [[ ! "$CUSTOMER_ID" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
log_error "Customer ID must be alphanumeric (hyphens/underscores allowed): $CUSTOMER_ID"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Pre-flight checks
|
|
#-------------------------------------------------------------------------------
|
|
check_prerequisites() {
|
|
local missing=()
|
|
|
|
# Must be run from repo root (templates/ and templates.json must exist)
|
|
if [[ ! -f "templates.json" ]]; then
|
|
log_error "templates.json not found. Run this script from the app-catalog-felhom.eu repo root."
|
|
exit 1
|
|
fi
|
|
if [[ ! -d "templates" ]]; then
|
|
log_error "templates/ directory not found. Run this script from the app-catalog-felhom.eu repo root."
|
|
exit 1
|
|
fi
|
|
|
|
command -v jq >/dev/null 2>&1 || missing+=("jq")
|
|
command -v envsubst >/dev/null 2>&1 || missing+=("gettext-base (envsubst)")
|
|
command -v openssl >/dev/null 2>&1 || missing+=("openssl")
|
|
|
|
if [[ "$PUSH" == true ]]; then
|
|
command -v git >/dev/null 2>&1 || missing+=("git")
|
|
fi
|
|
|
|
if [[ ${#missing[@]} -gt 0 ]]; then
|
|
log_error "Missing required tools: ${missing[*]}"
|
|
log_error "Install with: sudo apt install ${missing[*]}"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Determine app list
|
|
#-------------------------------------------------------------------------------
|
|
resolve_apps() {
|
|
local available_apps=()
|
|
|
|
# Discover available apps from templates/ directory
|
|
for dir in templates/*/; do
|
|
[[ -f "${dir}docker-compose.yml" ]] && available_apps+=("$(basename "$dir")")
|
|
done
|
|
|
|
if [[ -z "$APP_LIST" ]]; then
|
|
# Default: all available apps
|
|
APPS=("${available_apps[@]}")
|
|
else
|
|
# Parse comma-separated list
|
|
IFS=',' read -ra APPS <<< "$APP_LIST"
|
|
# Validate each app exists
|
|
for app in "${APPS[@]}"; do
|
|
local found=false
|
|
for avail in "${available_apps[@]}"; do
|
|
[[ "$app" == "$avail" ]] && { found=true; break; }
|
|
done
|
|
if [[ "$found" == false ]]; then
|
|
log_error "Unknown app: $app"
|
|
log_error "Available: ${available_apps[*]}"
|
|
exit 1
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Warn about HDD_PATH requirements
|
|
if [[ -z "$HDD_PATH" ]]; then
|
|
local hdd_apps=()
|
|
for app in "${APPS[@]}"; do
|
|
if [[ " $APPS_NEEDING_HDD " == *" $app "* ]]; then
|
|
hdd_apps+=("$app")
|
|
fi
|
|
done
|
|
if [[ ${#hdd_apps[@]} -gt 0 ]]; then
|
|
log_warn "No --hdd-path provided. These apps will require manual HDD_PATH input in Portainer:"
|
|
log_warn " ${hdd_apps[*]}"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Secret generation
|
|
#-------------------------------------------------------------------------------
|
|
generate_secret() {
|
|
local type="$1"
|
|
local param="$2"
|
|
|
|
case "$type" in
|
|
password)
|
|
openssl rand -base64 $(( param * 2 )) 2>/dev/null \
|
|
| tr -dc 'a-zA-Z0-9' \
|
|
| head -c "$param"
|
|
;;
|
|
hex)
|
|
openssl rand -hex "$param" 2>/dev/null
|
|
;;
|
|
static)
|
|
echo "$param"
|
|
;;
|
|
*)
|
|
log_error "Unknown secret type: $type"
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Load or generate secrets for a customer
|
|
#
|
|
# Reads existing secrets.env to preserve previously generated values.
|
|
# Only generates new secrets for variables not already present.
|
|
#-------------------------------------------------------------------------------
|
|
load_or_generate_secrets() {
|
|
local customer_dir="$1"
|
|
local secrets_file="${customer_dir}/secrets.env"
|
|
|
|
# Load existing secrets (if re-running)
|
|
declare -gA SECRETS=()
|
|
if [[ -f "$secrets_file" ]]; then
|
|
log_info "Loading existing secrets from ${secrets_file}"
|
|
while IFS='=' read -r key value; do
|
|
[[ "$key" =~ ^[[:space:]]*# ]] && continue
|
|
[[ -z "$key" ]] && continue
|
|
key="${key#"${key%%[![:space:]]*}"}"
|
|
key="${key%"${key##*[![:space:]]}"}"
|
|
[[ -n "$key" && -n "$value" ]] && SECRETS[$key]="$value"
|
|
done < "$secrets_file"
|
|
fi
|
|
|
|
local generated=0
|
|
local preserved=0
|
|
|
|
# Generate missing secrets for requested apps
|
|
for def in "${APP_SECRET_DEFS[@]}"; do
|
|
IFS=':' read -r def_app def_var def_type def_param <<< "$def"
|
|
|
|
# Skip apps not in the customer's list
|
|
local in_list=false
|
|
for app in "${APPS[@]}"; do
|
|
[[ "$app" == "$def_app" ]] && { in_list=true; break; }
|
|
done
|
|
[[ "$in_list" == false ]] && continue
|
|
|
|
local key="${def_app}__${def_var}"
|
|
if [[ -n "${SECRETS[$key]+x}" ]]; then
|
|
log_debug " ${def_app}/${def_var}: preserved"
|
|
(( preserved++ )) || true
|
|
else
|
|
SECRETS[$key]=$(generate_secret "$def_type" "$def_param")
|
|
log_debug " ${def_app}/${def_var}: generated (${def_type}:${def_param})"
|
|
(( generated++ )) || true
|
|
fi
|
|
done
|
|
|
|
log_success "Secrets: ${generated} generated, ${preserved} preserved"
|
|
|
|
# Write secrets.env
|
|
if [[ "$DRY_RUN" == false ]]; then
|
|
cat > "$secrets_file" << EOF
|
|
# Customer: ${CUSTOMER_ID}
|
|
# Domain: ${DOMAIN}
|
|
# Generated by generate-customer.sh v${SCRIPT_VERSION}
|
|
# Generated: $(date -u '+%Y-%m-%d %H:%M:%S UTC')
|
|
#
|
|
# This file is a reference copy of all generated secrets.
|
|
# Actual values are baked into the compose files and templates.json.
|
|
|
|
# Global
|
|
DOMAIN=${DOMAIN}
|
|
$(if [[ -n "$HDD_PATH" ]]; then echo "HDD_PATH=${HDD_PATH}"; else echo "# HDD_PATH= (not set)"; fi)
|
|
EOF
|
|
# Write per-app secrets in order (namespaced to avoid collisions)
|
|
local current_app=""
|
|
for def in "${APP_SECRET_DEFS[@]}"; do
|
|
IFS=':' read -r def_app def_var _ _ <<< "$def"
|
|
local in_list=false
|
|
for app in "${APPS[@]}"; do
|
|
[[ "$app" == "$def_app" ]] && { in_list=true; break; }
|
|
done
|
|
[[ "$in_list" == false ]] && continue
|
|
|
|
if [[ "$def_app" != "$current_app" ]]; then
|
|
echo "" >> "$secrets_file"
|
|
echo "# ${def_app}" >> "$secrets_file"
|
|
current_app="$def_app"
|
|
fi
|
|
# Namespaced key: app__VAR (matches internal SECRETS[] keys)
|
|
echo "${def_app}__${def_var}=${SECRETS[${def_app}__${def_var}]}" >> "$secrets_file"
|
|
done
|
|
|
|
chmod 600 "$secrets_file"
|
|
fi
|
|
}
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Generate compose files with baked-in values
|
|
#-------------------------------------------------------------------------------
|
|
generate_compose_files() {
|
|
local customer_dir="$1"
|
|
|
|
for app in "${APPS[@]}"; do
|
|
local src="templates/${app}/docker-compose.yml"
|
|
local dst_dir="${customer_dir}/${app}"
|
|
local dst="${dst_dir}/docker-compose.yml"
|
|
|
|
if [[ ! -f "$src" ]]; then
|
|
log_error "Template not found: $src"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
log_info "[DRY RUN] Would generate: ${dst}"
|
|
continue
|
|
fi
|
|
|
|
mkdir -p "$dst_dir"
|
|
|
|
# Step 1: Normalize {{VAR}} → ${VAR} (handle old mustache syntax)
|
|
local tmpfile
|
|
tmpfile=$(mktemp)
|
|
sed 's/{{DOMAIN}}/${DOMAIN}/g; s/{{HDD_PATH}}/${HDD_PATH}/g' "$src" > "$tmpfile"
|
|
|
|
# Step 2: Build envsubst variable list (only substitute known values)
|
|
local subst_vars='${DOMAIN}'
|
|
export DOMAIN
|
|
|
|
if [[ -n "$HDD_PATH" ]]; then
|
|
subst_vars+=' ${HDD_PATH}'
|
|
export HDD_PATH
|
|
fi
|
|
|
|
# Export app-specific secrets
|
|
for def in "${APP_SECRET_DEFS[@]}"; do
|
|
IFS=':' read -r def_app def_var _ _ <<< "$def"
|
|
[[ "$def_app" != "$app" ]] && continue
|
|
|
|
local key="${def_app}__${def_var}"
|
|
if [[ -n "${SECRETS[$key]+x}" ]]; then
|
|
export "$def_var"="${SECRETS[$key]}"
|
|
subst_vars+=" \${${def_var}}"
|
|
fi
|
|
done
|
|
|
|
log_debug " ${app}: subst_vars=${subst_vars}"
|
|
|
|
# Step 3: envsubst with explicit var list (leaves unresolved vars intact)
|
|
envsubst "$subst_vars" < "$tmpfile" > "$dst"
|
|
rm -f "$tmpfile"
|
|
|
|
# Step 4: Unset exported app-specific secrets (avoid leaking to next app)
|
|
for def in "${APP_SECRET_DEFS[@]}"; do
|
|
IFS=':' read -r def_app def_var _ _ <<< "$def"
|
|
[[ "$def_app" != "$app" ]] && continue
|
|
unset "$def_var" 2>/dev/null || true
|
|
done
|
|
|
|
log_debug " ${app}: generated"
|
|
done
|
|
}
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Generate customer-specific templates.json
|
|
#
|
|
# Takes the master templates.json, filters to requested apps, updates
|
|
# repository URLs to point at the customers repo, and removes env vars
|
|
# that have been baked into compose files.
|
|
#-------------------------------------------------------------------------------
|
|
generate_templates_json() {
|
|
local customer_dir="$1"
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
log_info "[DRY RUN] Would generate: ${customer_dir}/templates.json"
|
|
return
|
|
fi
|
|
|
|
# Build jq filter for app titles
|
|
# We need to match by stackfile path since titles might have different casing
|
|
local app_filters=()
|
|
for app in "${APPS[@]}"; do
|
|
app_filters+=("\"templates/${app}/docker-compose.yml\"")
|
|
done
|
|
local stackfile_array
|
|
stackfile_array=$(printf '%s,' "${app_filters[@]}")
|
|
stackfile_array="[${stackfile_array%,}]"
|
|
|
|
local keep_hdd_json="false"
|
|
[[ -z "$HDD_PATH" ]] && keep_hdd_json="true"
|
|
|
|
jq --arg customer "$CUSTOMER_ID" \
|
|
--arg repo "$CUSTOMERS_REPO_RAW" \
|
|
--argjson stackfiles "$stackfile_array" \
|
|
--argjson keep_hdd "$keep_hdd_json" \
|
|
'{
|
|
version: .version,
|
|
templates: [
|
|
.templates[]
|
|
| select(.repository.stackfile as $sf | $stackfiles | index($sf))
|
|
| .repository.url = $repo
|
|
| .repository.stackfile = ($customer + "/" + (.repository.stackfile | split("/") | .[1:] | join("/")))
|
|
| .env = (
|
|
if $keep_hdd then
|
|
[.env[]? | select(.name == "HDD_PATH")]
|
|
else
|
|
[]
|
|
end
|
|
)
|
|
]
|
|
}' templates.json > "${customer_dir}/templates.json"
|
|
|
|
log_debug " templates.json: $(jq '.templates | length' "${customer_dir}/templates.json") app entries"
|
|
}
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Git push to customers repo
|
|
#-------------------------------------------------------------------------------
|
|
git_push() {
|
|
local customer_dir="$1"
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
log_info "[DRY RUN] Would push to ${CUSTOMERS_REPO_GIT}"
|
|
return
|
|
fi
|
|
|
|
local repo_dir="${OUTPUT_BASE}/.git-repo"
|
|
|
|
# Clone or update the customers repo
|
|
if [[ -d "${repo_dir}/.git" ]]; then
|
|
log_info "Updating existing customers repo clone..."
|
|
cd "$repo_dir"
|
|
git pull --rebase --quiet 2>/dev/null || {
|
|
log_warn "Pull failed, continuing with local state"
|
|
}
|
|
cd - > /dev/null
|
|
else
|
|
log_info "Cloning customers repo..."
|
|
mkdir -p "$repo_dir"
|
|
git clone --quiet "$CUSTOMERS_REPO_GIT" "$repo_dir" 2>/dev/null || {
|
|
# Repo might not exist yet — init a fresh one
|
|
log_info "Repo doesn't exist yet, initializing..."
|
|
cd "$repo_dir"
|
|
git init --quiet
|
|
git remote add origin "$CUSTOMERS_REPO_GIT"
|
|
# Create initial commit so we have a branch
|
|
echo "# Felhom Customer Deployments" > README.md
|
|
echo "" >> README.md
|
|
echo "Auto-generated Portainer templates and compose files per customer." >> README.md
|
|
echo "**Do not edit manually** — regenerate with generate-customer.sh from app-catalog-felhom.eu." >> README.md
|
|
git add .
|
|
git commit --quiet -m "Initial commit"
|
|
cd - > /dev/null
|
|
}
|
|
fi
|
|
|
|
# Copy customer directory to repo
|
|
log_info "Copying ${CUSTOMER_ID}/ to repo..."
|
|
rm -rf "${repo_dir}/${CUSTOMER_ID}"
|
|
cp -r "$customer_dir" "${repo_dir}/${CUSTOMER_ID}"
|
|
|
|
# Commit and push
|
|
cd "$repo_dir"
|
|
git add "${CUSTOMER_ID}/"
|
|
if git diff --cached --quiet 2>/dev/null; then
|
|
log_info "No changes to push (already up to date)"
|
|
else
|
|
git commit --quiet -m "Update ${CUSTOMER_ID}: $(date -u '+%Y-%m-%d %H:%M UTC')"
|
|
git push --quiet origin HEAD 2>/dev/null && \
|
|
log_success "Pushed to ${CUSTOMERS_REPO_GIT}" || \
|
|
log_error "Push failed — check git credentials and repo access"
|
|
fi
|
|
cd - > /dev/null
|
|
}
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Print summary
|
|
#-------------------------------------------------------------------------------
|
|
print_summary() {
|
|
local customer_dir="$1"
|
|
local templates_url="${CUSTOMERS_REPO_RAW}/raw/branch/main/${CUSTOMER_ID}/templates.json"
|
|
|
|
echo ""
|
|
echo -e "${BOLD}${GREEN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD}${GREEN} Customer generated: ${CUSTOMER_ID}${NC}"
|
|
echo -e "${BOLD}${GREEN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
echo -e "${BOLD}Customer:${NC} ${CUSTOMER_ID}"
|
|
echo -e "${BOLD}Domain:${NC} ${DOMAIN}"
|
|
if [[ -n "$HDD_PATH" ]]; then
|
|
echo -e "${BOLD}HDD Path:${NC} ${HDD_PATH}"
|
|
else
|
|
echo -e "${BOLD}HDD Path:${NC} ${YELLOW}not set (apps will prompt in Portainer)${NC}"
|
|
fi
|
|
echo -e "${BOLD}Apps:${NC} ${APPS[*]}"
|
|
echo ""
|
|
echo -e "${BOLD}Output:${NC}"
|
|
echo " ${customer_dir}/"
|
|
for app in "${APPS[@]}"; do
|
|
echo " ├── ${app}/docker-compose.yml"
|
|
done
|
|
echo " ├── templates.json"
|
|
echo " └── secrets.env"
|
|
echo ""
|
|
echo -e "${BOLD}Portainer templates URL:${NC}"
|
|
echo " ${templates_url}"
|
|
echo ""
|
|
echo -e "${BOLD}docker-setup.sh usage:${NC}"
|
|
echo " sudo ./docker-setup.sh --domain ${DOMAIN} --customer ${CUSTOMER_ID} \\"
|
|
echo " --email certs@felhom.eu --cf-token <token>"
|
|
echo ""
|
|
if [[ -z "$HDD_PATH" ]]; then
|
|
echo -e "${YELLOW}Note: HDD_PATH not set. Apps requiring it (filebrowser, immich,"
|
|
echo -e "paperless-ngx, romm) will show an HDD Path field in Portainer.${NC}"
|
|
echo ""
|
|
fi
|
|
echo -e "${BOLD}Secrets reference:${NC}"
|
|
echo " ${customer_dir}/secrets.env"
|
|
echo ""
|
|
}
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Main
|
|
#-------------------------------------------------------------------------------
|
|
main() {
|
|
parse_args "$@"
|
|
check_prerequisites
|
|
resolve_apps
|
|
|
|
local customer_dir="${OUTPUT_BASE}/${CUSTOMER_ID}"
|
|
|
|
# Print plan
|
|
echo ""
|
|
echo -e "${BOLD}Generation plan:${NC}"
|
|
echo " Customer: ${CUSTOMER_ID}"
|
|
echo " Domain: ${DOMAIN}"
|
|
echo " HDD Path: ${HDD_PATH:-<not set>}"
|
|
echo " Apps: ${APPS[*]}"
|
|
echo " Output: ${customer_dir}/"
|
|
echo " Push: ${PUSH}"
|
|
echo ""
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
echo -e "${YELLOW}DRY RUN — no files will be created${NC}"
|
|
echo ""
|
|
fi
|
|
|
|
# Create output directory
|
|
if [[ "$DRY_RUN" == false ]]; then
|
|
mkdir -p "$customer_dir"
|
|
fi
|
|
|
|
log_step "1/4 — Generating secrets..."
|
|
load_or_generate_secrets "$customer_dir"
|
|
|
|
log_step "2/4 — Generating compose files..."
|
|
generate_compose_files "$customer_dir"
|
|
|
|
log_step "3/4 — Generating templates.json..."
|
|
generate_templates_json "$customer_dir"
|
|
|
|
if [[ "$PUSH" == true ]]; then
|
|
log_step "4/4 — Pushing to customers repo..."
|
|
git_push "$customer_dir"
|
|
else
|
|
log_step "4/4 — Skipping push (use --push to upload)"
|
|
fi
|
|
|
|
print_summary "$customer_dir"
|
|
}
|
|
|
|
main "$@" |