architecture changed to preconfigured customer templates
This commit is contained in:
@@ -1,12 +1,13 @@
|
|||||||
# Felhom App Catalog
|
# Felhom App Catalog
|
||||||
|
|
||||||
Central repository for all Felhom customer application deployments.
|
Central repository for Felhom customer application templates.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
felhom-app-catalog/ ← This repo (source of truth)
|
app-catalog-felhom.eu/ <- This repo (source of truth)
|
||||||
├── templates/ # Docker Compose templates with placeholders
|
├── templates.json # Portainer App Templates index (generic, with env var prompts)
|
||||||
|
├── templates/ # Docker Compose templates with ${VAR} env var syntax
|
||||||
│ ├── actualbudget/
|
│ ├── actualbudget/
|
||||||
│ ├── docmost/
|
│ ├── docmost/
|
||||||
│ ├── filebrowser/
|
│ ├── filebrowser/
|
||||||
@@ -17,121 +18,186 @@ felhom-app-catalog/ ← This repo (source of truth)
|
|||||||
│ ├── romm/
|
│ ├── romm/
|
||||||
│ ├── stirling-pdf/
|
│ ├── stirling-pdf/
|
||||||
│ └── vaultwarden/
|
│ └── vaultwarden/
|
||||||
├── customers/ # Per-customer configuration (YAML)
|
└── scripts/
|
||||||
│ ├── demo-felhom.yaml
|
└── generate-customer.sh # Generates customer-specific templates with baked-in secrets
|
||||||
│ └── pi-customer-1.yaml
|
|
||||||
├── scripts/
|
|
||||||
│ └── render.sh # Renders output from templates + customer configs
|
|
||||||
└── output/ # Generated monorepo (pushed to Gitea)
|
|
||||||
├── README.md
|
|
||||||
├── demo-felhom/
|
|
||||||
│ ├── actualbudget/docker-compose.yml
|
|
||||||
│ ├── immich/docker-compose.yml
|
|
||||||
│ └── ...
|
|
||||||
└── pi-customer-1/
|
|
||||||
├── actualbudget/docker-compose.yml
|
|
||||||
└── ...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The `output/` directory is what gets pushed to:
|
The output is pushed to:
|
||||||
**https://gitea.dooplex.hu/admin/customers-felhom.eu**
|
**https://gitea.dooplex.hu/admin/customers-felhom.eu**
|
||||||
|
|
||||||
|
```
|
||||||
|
customers-felhom.eu/ <- Generated per-customer deployments
|
||||||
|
├── demo-felhom/
|
||||||
|
│ ├── templates.json # Customer Portainer templates (zero env vars, zero-touch deploy)
|
||||||
|
│ ├── secrets.env # Reference copy of all generated secrets
|
||||||
|
│ ├── actualbudget/docker-compose.yml
|
||||||
|
│ ├── docmost/docker-compose.yml
|
||||||
|
│ └── ...
|
||||||
|
└── pi-customer-1/
|
||||||
|
├── templates.json
|
||||||
|
├── secrets.env
|
||||||
|
├── actualbudget/docker-compose.yml
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
1. **Templates** contain Docker Compose files with `{{DOMAIN}}` and `{{HDD_PATH}}` placeholders
|
1. **Templates** in this repo contain Docker Compose files with `${DOMAIN}`, `${HDD_PATH}`, and `${SECRET}` placeholders
|
||||||
2. **Customer configs** define which apps each customer gets, their domain, HDD path, and any version overrides
|
2. **`generate-customer.sh`** substitutes all values (domain, HDD path, auto-generated passwords) and produces customer-specific compose files + a Portainer `templates.json` with zero env vars
|
||||||
3. **render.sh** substitutes all placeholders and generates the output directory
|
3. **`docker-setup.sh`** on the customer node starts Portainer with `--templates` pointing at the customer's generated `templates.json`
|
||||||
4. **`--push`** commits and pushes the output to the Gitea monorepo
|
4. **Customer** opens Portainer -> Templates -> picks an app -> clicks Deploy -> **done**
|
||||||
5. **Portainer GitOps** on each customer node pulls from the same repo, using a different compose path per stack
|
|
||||||
|
|
||||||
### Placeholder Reference
|
|
||||||
|
|
||||||
| Placeholder | Source | Example |
|
|
||||||
|-------------|--------|---------|
|
|
||||||
| `{{DOMAIN}}` | `domain:` in customer YAML | `demo-felhom.eu` |
|
|
||||||
| `{{HDD_PATH}}` | `hdd_path:` in customer YAML | `/mnt/hdd_1` |
|
|
||||||
|
|
||||||
### Storage Strategy
|
|
||||||
|
|
||||||
- **HDD host paths** (`{{HDD_PATH}}/storage/...`): Large user data — photos, documents, ROMs
|
|
||||||
- **Named Docker volumes** (on NVMe): Databases, app config, caches — need fast I/O
|
|
||||||
- Templates that don't use `{{HDD_PATH}}` work without it (e.g. ActualBudget, Mealie)
|
|
||||||
- If a template needs `{{HDD_PATH}}` but the customer config doesn't set `hdd_path:`, the render script refuses that app and tells you what to fix
|
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
### Render & push
|
### 1. Generate customer templates (on your workstation)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/render.sh # Render all customers locally
|
# Full setup: all apps, with HDD
|
||||||
./scripts/render.sh --push # Render + commit + push to Gitea
|
./scripts/generate-customer.sh --customer demo-felhom \
|
||||||
./scripts/render.sh --customer demo-felhom # Render one customer only
|
--domain demo-felhom.eu --hdd-path /mnt/hdd_1 --push
|
||||||
./scripts/render.sh --dry-run # Preview what would happen
|
|
||||||
./scripts/render.sh --debug # Verbose output
|
# Raspberry Pi: lightweight apps only, no HDD yet
|
||||||
|
./scripts/generate-customer.sh --customer pi-customer-1 \
|
||||||
|
--domain pi-customer-1.local \
|
||||||
|
--apps actualbudget,filebrowser,mealie,stirling-pdf,vaultwarden --push
|
||||||
|
|
||||||
|
# Preview without creating files
|
||||||
|
./scripts/generate-customer.sh --customer test --domain test.local --dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
The default Gitea repo URL is `https://gitea.dooplex.hu/admin/customers-felhom.eu.git`.
|
Options:
|
||||||
Override with: `GITEA_REPO_URL=https://... ./scripts/render.sh --push`
|
- `--customer ID` -- Customer identifier (required)
|
||||||
|
- `--domain DOMAIN` -- Customer domain (required)
|
||||||
|
- `--hdd-path PATH` -- External HDD mount path (optional; apps needing it show a field in Portainer if omitted)
|
||||||
|
- `--apps LIST` -- Comma-separated app list (default: all available)
|
||||||
|
- `--push` -- Git commit and push to customers-felhom.eu repo
|
||||||
|
- `--dry-run` -- Show what would be done
|
||||||
|
- `--debug` -- Verbose output
|
||||||
|
|
||||||
### Adding a new app to the catalog
|
### 2. Set up customer server
|
||||||
|
|
||||||
1. Create `templates/<appname>/docker-compose.yml` using `{{DOMAIN}}` and optionally `{{HDD_PATH}}`
|
```bash
|
||||||
2. Add the app name to relevant customer configs in `customers/`
|
sudo ./docker-setup.sh --domain demo-felhom.eu --customer demo-felhom \
|
||||||
3. Run `./scripts/render.sh --push`
|
--email certs@felhom.eu --cf-token <cloudflare-api-token>
|
||||||
|
|
||||||
### Updating an app version
|
|
||||||
|
|
||||||
1. Edit the image tag in `templates/<appname>/docker-compose.yml`
|
|
||||||
2. Run `./scripts/render.sh --push`
|
|
||||||
3. Portainer auto-detects git changes and redeploys (if polling enabled)
|
|
||||||
|
|
||||||
Customers with version overrides keep their pinned version.
|
|
||||||
|
|
||||||
### Adding a new customer
|
|
||||||
|
|
||||||
1. Create `customers/<customer-id>.yaml` (copy an existing one as template)
|
|
||||||
2. Run `./scripts/render.sh --push`
|
|
||||||
3. Set up Portainer GitOps stacks on the customer node (see below)
|
|
||||||
|
|
||||||
## Portainer Stack Setup (per app)
|
|
||||||
|
|
||||||
On the customer's Portainer, for each app:
|
|
||||||
|
|
||||||
1. **Stacks → Add Stack → Repository**
|
|
||||||
2. Repository URL: `https://gitea.dooplex.hu/admin/customers-felhom.eu`
|
|
||||||
3. Compose path: `<customer-id>/<appname>/docker-compose.yml`
|
|
||||||
- Example: `demo-felhom/immich/docker-compose.yml`
|
|
||||||
4. Add environment variables (secrets — DB passwords, API keys, etc.)
|
|
||||||
5. Enable **GitOps auto-update** (optional, 5-minute polling)
|
|
||||||
6. Deploy
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
Secrets are **never stored in Git**. They live in Portainer's stack environment
|
|
||||||
variables on each customer node. Each template documents required env vars
|
|
||||||
in comments at the top of the compose file.
|
|
||||||
|
|
||||||
## Version Pinning
|
|
||||||
|
|
||||||
In a customer YAML, you can pin specific app versions:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
overrides:
|
|
||||||
immich_version: "v2.4.1" # Don't auto-update Immich for this customer
|
|
||||||
auto_update: false # Skip ALL updates for this customer
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 3. Deploy apps
|
||||||
|
|
||||||
|
Open `https://portainer.<domain>` -> **Templates** -> pick an app -> **Deploy the stack**.
|
||||||
|
|
||||||
|
All fields (domain, passwords, secrets) are pre-filled. If HDD_PATH wasn't set during generation, apps that need it will show one field to fill in.
|
||||||
|
|
||||||
|
### Re-generating after changes
|
||||||
|
|
||||||
|
Re-running `generate-customer.sh` for an existing customer preserves all previously generated secrets (idempotent). Only new apps or new secrets are generated.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add an app to an existing customer
|
||||||
|
./scripts/generate-customer.sh --customer demo-felhom \
|
||||||
|
--domain demo-felhom.eu --hdd-path /mnt/hdd_1 --push
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables & Secrets
|
||||||
|
|
||||||
|
Secrets are auto-generated by `generate-customer.sh` and baked directly into the compose files and Portainer templates. They are **not** stored separately on the customer node -- they live in the customers-felhom.eu Git repo (which is private on your Gitea).
|
||||||
|
|
||||||
|
A `secrets.env` reference file is generated alongside the compose files for easy lookup.
|
||||||
|
|
||||||
|
### Secret definitions
|
||||||
|
|
||||||
|
Defined in `generate-customer.sh` via the `APP_SECRET_DEFS` array:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
APP_SECRET_DEFS=(
|
||||||
|
"docmost:APP_SECRET:hex:32"
|
||||||
|
"docmost:DB_PASSWORD:password:24"
|
||||||
|
"immich:DB_PASSWORD:password:24"
|
||||||
|
"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:DB_PASSWORD:password:24"
|
||||||
|
"romm:MYSQL_ROOT_PASSWORD:password:24"
|
||||||
|
"romm:ROMM_AUTH_SECRET_KEY:hex:32"
|
||||||
|
"vaultwarden:ADMIN_TOKEN:hex:32"
|
||||||
|
"vaultwarden:SIGNUPS_ALLOWED:static:true"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Types: `password:LENGTH` (alphanumeric), `hex:LENGTH` (crypto), `static:VALUE` (fixed).
|
||||||
|
|
||||||
|
### Variable types per app
|
||||||
|
|
||||||
|
| App | DOMAIN | HDD_PATH | Secrets |
|
||||||
|
|-----|:------:|:--------:|---------|
|
||||||
|
| ActualBudget | yes | -- | -- |
|
||||||
|
| Docmost | yes | -- | APP_SECRET, DB_PASSWORD |
|
||||||
|
| FileBrowser | yes | yes | -- |
|
||||||
|
| Homebox | yes | -- | -- |
|
||||||
|
| Immich | yes | yes | DB_PASSWORD |
|
||||||
|
| Mealie | yes | -- | -- |
|
||||||
|
| Paperless-ngx | yes | yes | PAPERLESS_SECRET_KEY, DB_PASSWORD, PAPERLESS_ADMIN_USER, PAPERLESS_ADMIN_PASSWORD |
|
||||||
|
| ROMM | yes | yes | DB_PASSWORD, MYSQL_ROOT_PASSWORD, ROMM_AUTH_SECRET_KEY |
|
||||||
|
| Stirling-PDF | yes | -- | -- |
|
||||||
|
| Vaultwarden | yes | -- | ADMIN_TOKEN |
|
||||||
|
|
||||||
## App Catalog
|
## App Catalog
|
||||||
|
|
||||||
| App | DB Type | RAM | Pi | HDD Data | Subdomain |
|
| App | DB Type | RAM | Pi | HDD Data | Subdomain |
|
||||||
|-----|---------|-----|-----|----------|-----------|
|
|-----|---------|-----|-----|----------|-----------|
|
||||||
| ActualBudget | None (file) | ~50MB | ✅ | — | budget.* |
|
| ActualBudget | None (file) | ~50MB | yes | -- | budget.* |
|
||||||
| Docmost | PostgreSQL + Redis | ~200MB | ⚠️ | — | docs.* |
|
| Docmost | PostgreSQL + Redis | ~200MB | maybe | -- | docs.* |
|
||||||
| FileBrowser | None (file) | ~30MB | ✅ | `{{HDD_PATH}}/` | files.* |
|
| FileBrowser | None (file) | ~30MB | yes | `${HDD_PATH}/storage/filebrowser/` | files.* |
|
||||||
| Homebox | None (SQLite) | ~50MB | ✅ | — | inventory.* |
|
| Homebox | None (SQLite) | ~50MB | yes | -- | inventory.* |
|
||||||
| Immich | PostgreSQL + Redis | ~4GB | ❌ | `{{HDD_PATH}}/storage/immich/` | photos.* |
|
| Immich | PostgreSQL + Redis | ~4GB | no | `${HDD_PATH}/storage/immich/` | photos.* |
|
||||||
| Mealie | None (SQLite) | ~200MB | ✅ | — | recipes.* |
|
| Mealie | None (SQLite) | ~200MB | yes | -- | recipes.* |
|
||||||
| Paperless-ngx | PostgreSQL + Redis | ~500MB | ✅ | `{{HDD_PATH}}/storage/paperless/` | paperless.* |
|
| Paperless-ngx | PostgreSQL + Redis | ~500MB | yes | `${HDD_PATH}/storage/paperless/` | paperless.* |
|
||||||
| ROMM | MariaDB + Redis | ~300MB | ⚠️ | `{{HDD_PATH}}/storage/romm/` | arcade.* |
|
| ROMM | MariaDB + Redis | ~300MB | maybe | `${HDD_PATH}/storage/romm/` | arcade.* |
|
||||||
| Stirling-PDF | None | ~200MB | ✅ | — | pdf.* |
|
| Stirling-PDF | None | ~200MB | yes | -- | pdf.* |
|
||||||
| Vaultwarden | None (SQLite) | ~50MB | ✅ | — | vault.* |
|
| Vaultwarden | None (SQLite) | ~50MB | yes | -- | vault.* |
|
||||||
|
|
||||||
|
### Storage strategy
|
||||||
|
|
||||||
|
- **HDD host paths** (`${HDD_PATH}/storage/...`): Large user data -- photos, documents, ROMs
|
||||||
|
- **Named Docker volumes** (on internal SSD): Databases, app config, caches -- need fast I/O
|
||||||
|
- Templates without `${HDD_PATH}` work without an external HDD (e.g., ActualBudget, Mealie)
|
||||||
|
|
||||||
|
## Adding a New App
|
||||||
|
|
||||||
|
1. Create `templates/<appname>/docker-compose.yml` using `${DOMAIN}` and optionally `${HDD_PATH}`
|
||||||
|
2. Add a template entry in `templates.json` with env definitions, description, logo, and notes
|
||||||
|
3. If the app needs secrets, add entries to `APP_SECRET_DEFS` in `generate-customer.sh`
|
||||||
|
4. Re-run `generate-customer.sh --push` for each customer
|
||||||
|
|
||||||
|
### templates.json entry format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": 3,
|
||||||
|
"title": "App Name",
|
||||||
|
"description": "Short description.",
|
||||||
|
"categories": ["category"],
|
||||||
|
"platform": "linux",
|
||||||
|
"logo": "https://example.com/logo.png",
|
||||||
|
"note": "<b>Access:</b> https://subdomain.<DOMAIN>",
|
||||||
|
"repository": {
|
||||||
|
"url": "https://gitea.dooplex.hu/admin/app-catalog-felhom.eu",
|
||||||
|
"stackfile": "templates/appname/docker-compose.yml"
|
||||||
|
},
|
||||||
|
"env": [
|
||||||
|
{
|
||||||
|
"name": "DOMAIN",
|
||||||
|
"label": "Domain",
|
||||||
|
"description": "Your server domain (e.g., demo-felhom.eu)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Repositories
|
||||||
|
|
||||||
|
| Repository | Purpose |
|
||||||
|
|------------|---------|
|
||||||
|
| [app-catalog-felhom.eu](https://gitea.dooplex.hu/admin/app-catalog-felhom.eu) | This repo -- templates + generation script |
|
||||||
|
| [customers-felhom.eu](https://gitea.dooplex.hu/admin/customers-felhom.eu) | Generated per-customer compose files + templates |
|
||||||
|
| [deploy-portainer](https://gitea.dooplex.hu/admin/deploy-portainer) | `docker-setup.sh` -- server provisioning |
|
||||||
@@ -0,0 +1,658 @@
|
|||||||
|
#!/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 "$@"
|
||||||
Reference in New Issue
Block a user