updated to v2.0, monorepo customer output
This commit is contained in:
@@ -5,31 +5,44 @@ Central repository for all Felhom customer application deployments.
|
|||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
felhom-app-catalog/
|
felhom-app-catalog/ ← This repo (source of truth)
|
||||||
├── templates/ # Docker Compose templates ({{DOMAIN}} placeholder)
|
├── templates/ # Docker Compose templates with placeholders
|
||||||
│ ├── actualbudget/
|
│ ├── actualbudget/
|
||||||
│ ├── docmost/
|
│ ├── docmost/
|
||||||
│ ├── filebrowser/
|
│ ├── filebrowser/
|
||||||
│ ├── homebox/
|
│ ├── homebox/
|
||||||
│ ├── immich/
|
│ ├── immich/
|
||||||
│ ├── mealie/
|
│ ├── mealie/
|
||||||
|
│ ├── paperless-ngx/
|
||||||
│ ├── romm/
|
│ ├── romm/
|
||||||
│ ├── stirling-pdf/
|
│ ├── stirling-pdf/
|
||||||
│ └── vaultwarden/
|
│ └── vaultwarden/
|
||||||
├── customers/ # Per-customer configuration
|
├── customers/ # Per-customer configuration (YAML)
|
||||||
│ ├── demo-felhom.yaml
|
│ ├── demo-felhom.yaml
|
||||||
│ └── pi-customer-1.yaml
|
│ └── pi-customer-1.yaml
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ └── render.sh # Renders customer repos from templates
|
│ └── render.sh # Renders output from templates + customer configs
|
||||||
└── README.md
|
└── 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:
|
||||||
|
**https://gitea.dooplex.hu/admin/customers-felhom.eu**
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
1. **Templates** contain Docker Compose files with `{{DOMAIN}}` and `{{HDD_PATH}}` placeholders
|
1. **Templates** contain Docker Compose files with `{{DOMAIN}}` and `{{HDD_PATH}}` placeholders
|
||||||
2. **Customer configs** define which apps each customer gets, their domain, HDD path, and any version overrides
|
2. **Customer configs** define which apps each customer gets, their domain, HDD path, and any version overrides
|
||||||
3. **render.sh** generates per-customer Gitea repos with all placeholders substituted
|
3. **render.sh** substitutes all placeholders and generates the output directory
|
||||||
4. **Portainer GitOps** on each customer node pulls from their repo and deploys
|
4. **`--push`** commits and pushes the output to the Gitea monorepo
|
||||||
|
5. **Portainer GitOps** on each customer node pulls from the same repo, using a different compose path per stack
|
||||||
|
|
||||||
### Placeholder Reference
|
### Placeholder Reference
|
||||||
|
|
||||||
@@ -43,31 +56,51 @@ felhom-app-catalog/
|
|||||||
- **HDD host paths** (`{{HDD_PATH}}/storage/...`): Large user data — photos, documents, ROMs
|
- **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
|
- **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)
|
- 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/render.sh # Render all customers locally
|
||||||
|
./scripts/render.sh --push # Render + commit + push to Gitea
|
||||||
|
./scripts/render.sh --customer demo-felhom # Render one customer only
|
||||||
|
./scripts/render.sh --dry-run # Preview what would happen
|
||||||
|
./scripts/render.sh --debug # Verbose output
|
||||||
|
```
|
||||||
|
|
||||||
|
The default Gitea repo URL is `https://gitea.dooplex.hu/admin/customers-felhom.eu.git`.
|
||||||
|
Override with: `GITEA_REPO_URL=https://... ./scripts/render.sh --push`
|
||||||
|
|
||||||
### Adding a new app to the catalog
|
### Adding a new app to the catalog
|
||||||
1. Create `templates/<appname>/docker-compose.yml` using `{{DOMAIN}}` placeholder
|
|
||||||
2. Add to relevant customer configs in `customers/`
|
1. Create `templates/<appname>/docker-compose.yml` using `{{DOMAIN}}` and optionally `{{HDD_PATH}}`
|
||||||
3. Run `./scripts/render.sh` to regenerate customer repos
|
2. Add the app name to relevant customer configs in `customers/`
|
||||||
|
3. Run `./scripts/render.sh --push`
|
||||||
|
|
||||||
### Updating an app version
|
### Updating an app version
|
||||||
|
|
||||||
1. Edit the image tag in `templates/<appname>/docker-compose.yml`
|
1. Edit the image tag in `templates/<appname>/docker-compose.yml`
|
||||||
2. Run `./scripts/render.sh` — skips customers with version overrides
|
2. Run `./scripts/render.sh --push`
|
||||||
3. Portainer auto-detects git changes and redeploys (if polling enabled)
|
3. Portainer auto-detects git changes and redeploys (if polling enabled)
|
||||||
|
|
||||||
|
Customers with version overrides keep their pinned version.
|
||||||
|
|
||||||
### Adding a new customer
|
### Adding a new customer
|
||||||
1. Create `customers/<customer-id>.yaml`
|
|
||||||
2. Create the Gitea repo: `customers/<customer-id>-stacks`
|
1. Create `customers/<customer-id>.yaml` (copy an existing one as template)
|
||||||
3. Run `./scripts/render.sh --customer <customer-id>`
|
2. Run `./scripts/render.sh --push`
|
||||||
4. Set up Portainer GitOps stacks on the customer node
|
3. Set up Portainer GitOps stacks on the customer node (see below)
|
||||||
|
|
||||||
## Portainer Stack Setup (per app)
|
## Portainer Stack Setup (per app)
|
||||||
|
|
||||||
On the customer's Portainer:
|
On the customer's Portainer, for each app:
|
||||||
|
|
||||||
1. **Stacks → Add Stack → Repository**
|
1. **Stacks → Add Stack → Repository**
|
||||||
2. Repository URL: `https://gitea.felhom.eu/customers/<customer-id>-stacks`
|
2. Repository URL: `https://gitea.dooplex.hu/admin/customers-felhom.eu`
|
||||||
3. Compose path: `<appname>/docker-compose.yml`
|
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.)
|
4. Add environment variables (secrets — DB passwords, API keys, etc.)
|
||||||
5. Enable **GitOps auto-update** (optional, 5-minute polling)
|
5. Enable **GitOps auto-update** (optional, 5-minute polling)
|
||||||
6. Deploy
|
6. Deploy
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Customer: Demo / Test Server (N100 Mini PC)
|
# Customer: Demo / Test Server (N100 Mini PC)
|
||||||
# Hardware: Intel N100, 16GB RAM, 512GB NVMe + 1TB HDD
|
# Hardware: Intel N100, 16GB RAM, 128GB NVMe + 1TB HDD
|
||||||
# Network: Local + Cloudflare Tunnel for demo access
|
# Network: Local + Cloudflare Tunnel for demo access
|
||||||
|
|
||||||
customer_id: demo-felhom
|
customer_id: demo-felhom
|
||||||
|
|||||||
Executable → Regular
+202
-84
@@ -1,21 +1,35 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
#===============================================================================
|
#===============================================================================
|
||||||
# Felhom App Catalog - Customer Repo Renderer v1.0
|
# Felhom App Catalog - Customer Repo Renderer v2.0
|
||||||
#
|
#
|
||||||
# Reads customer YAML configs and generates per-customer stack directories
|
# Reads customer YAML configs and generates Docker Compose stacks
|
||||||
# by substituting {{DOMAIN}} placeholders in Docker Compose templates.
|
# by substituting {{DOMAIN}} and {{HDD_PATH}} placeholders in templates.
|
||||||
|
#
|
||||||
|
# Output structure (monorepo — all customers in one Gitea repo):
|
||||||
|
# output/
|
||||||
|
# ├── README.md
|
||||||
|
# ├── demo-felhom/
|
||||||
|
# │ ├── actualbudget/docker-compose.yml
|
||||||
|
# │ ├── immich/docker-compose.yml
|
||||||
|
# │ └── ...
|
||||||
|
# └── pi-customer-1/
|
||||||
|
# ├── actualbudget/docker-compose.yml
|
||||||
|
# └── ...
|
||||||
|
#
|
||||||
|
# Portainer GitOps compose path example:
|
||||||
|
# demo-felhom/actualbudget/docker-compose.yml
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./render.sh # Render all customers
|
# ./render.sh # Render all customers
|
||||||
# ./render.sh --customer demo-felhom # Render specific customer
|
# ./render.sh --customer demo-felhom # Render specific customer
|
||||||
# ./render.sh --dry-run # Show what would be done
|
# ./render.sh --dry-run # Show what would be done
|
||||||
# ./render.sh --push # Render + push to Gitea
|
# ./render.sh --push # Render + commit + push to Gitea
|
||||||
# ./render.sh --output-dir /tmp/out # Custom output directory
|
# ./render.sh --output-dir /tmp/out # Custom output directory
|
||||||
# ./render.sh --debug # Verbose output
|
# ./render.sh --debug # Verbose output
|
||||||
#
|
#
|
||||||
# Prerequisites:
|
# Prerequisites:
|
||||||
# - git (for push mode)
|
# - git (for push mode)
|
||||||
# - Access to Gitea (for push mode)
|
# - Access to Gitea repo (for push mode)
|
||||||
#
|
#
|
||||||
#===============================================================================
|
#===============================================================================
|
||||||
|
|
||||||
@@ -30,9 +44,8 @@ TEMPLATES_DIR="${CATALOG_DIR}/templates"
|
|||||||
CUSTOMERS_DIR="${CATALOG_DIR}/customers"
|
CUSTOMERS_DIR="${CATALOG_DIR}/customers"
|
||||||
DEFAULT_OUTPUT_DIR="${CATALOG_DIR}/output"
|
DEFAULT_OUTPUT_DIR="${CATALOG_DIR}/output"
|
||||||
|
|
||||||
# Gitea settings (for --push mode)
|
# Gitea monorepo settings (for --push mode)
|
||||||
GITEA_URL="${GITEA_URL:-https://gitea.felhom.eu}"
|
GITEA_REPO_URL="${GITEA_REPO_URL:-https://gitea.dooplex.hu/admin/customers-felhom.eu.git}"
|
||||||
GITEA_ORG="${GITEA_ORG:-customers}"
|
|
||||||
|
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
# Colors & Logging
|
# Colors & Logging
|
||||||
@@ -53,16 +66,13 @@ log_step() { echo -e "\n${BOLD}[STEP]${NC} $*"; }
|
|||||||
|
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
# Simple YAML parser (no external dependencies)
|
# Simple YAML parser (no external dependencies)
|
||||||
# Handles our flat customer YAML format
|
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
yaml_get_value() {
|
yaml_get_value() {
|
||||||
# Get a simple key: value from YAML file
|
|
||||||
local file="$1" key="$2"
|
local file="$1" key="$2"
|
||||||
grep -E "^${key}:" "$file" 2>/dev/null | sed "s/^${key}:[[:space:]]*//" | sed 's/^"//' | sed 's/"$//' | sed "s/^'//" | sed "s/'$//" || echo ""
|
grep -E "^${key}:" "$file" 2>/dev/null | sed "s/^${key}:[[:space:]]*//" | sed 's/^"//' | sed 's/"$//' | sed "s/^'//" | sed "s/'$//" || echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
yaml_get_list() {
|
yaml_get_list() {
|
||||||
# Get a YAML list (items starting with " - ")
|
|
||||||
local file="$1" key="$2"
|
local file="$1" key="$2"
|
||||||
local in_section=false
|
local in_section=false
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
@@ -73,19 +83,14 @@ yaml_get_list() {
|
|||||||
if $in_section; then
|
if $in_section; then
|
||||||
if [[ "$line" =~ ^[[:space:]]*-[[:space:]]+(.*) ]]; then
|
if [[ "$line" =~ ^[[:space:]]*-[[:space:]]+(.*) ]]; then
|
||||||
echo "${BASH_REMATCH[1]}" | sed 's/^"//' | sed 's/"$//'
|
echo "${BASH_REMATCH[1]}" | sed 's/^"//' | sed 's/"$//'
|
||||||
elif [[ "$line" =~ ^[a-zA-Z_] ]] || [[ -z "$line" && "$in_section" == true ]]; then
|
elif [[ "$line" =~ ^[a-zA-Z_] ]]; then
|
||||||
# New top-level key or empty line after list = end of section
|
break
|
||||||
# But only break on a new top-level key, not empty lines within
|
|
||||||
if [[ "$line" =~ ^[a-zA-Z_] ]]; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done < "$file"
|
done < "$file"
|
||||||
}
|
}
|
||||||
|
|
||||||
yaml_get_override() {
|
yaml_get_override() {
|
||||||
# Get a specific override value: overrides.key
|
|
||||||
local file="$1" key="$2"
|
local file="$1" key="$2"
|
||||||
local in_overrides=false
|
local in_overrides=false
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
@@ -108,7 +113,7 @@ yaml_get_override() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
# Template rendering
|
# Template rendering (one customer)
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
render_customer() {
|
render_customer() {
|
||||||
local customer_file="$1"
|
local customer_file="$1"
|
||||||
@@ -151,8 +156,11 @@ render_customer() {
|
|||||||
log_info "HDD path: $hdd_path"
|
log_info "HDD path: $hdd_path"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Output directory for this customer
|
# Output directory: output/<customer_id>/ (no -stacks suffix)
|
||||||
local customer_output="${output_base}/${customer_id}-stacks"
|
local customer_output="${output_base}/${customer_id}"
|
||||||
|
|
||||||
|
# Strip .git from repo URL for display
|
||||||
|
local repo_display="${GITEA_REPO_URL%.git}"
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
echo -e " ${CYAN}[DRY-RUN]${NC} Would create: ${customer_output}/"
|
echo -e " ${CYAN}[DRY-RUN]${NC} Would create: ${customer_output}/"
|
||||||
@@ -167,45 +175,48 @@ render_customer() {
|
|||||||
echo -e " ${YELLOW}[DRY-RUN]${NC} ↳ ⚠ Template uses {{HDD_PATH}} but hdd_path not set!"
|
echo -e " ${YELLOW}[DRY-RUN]${NC} ↳ ⚠ Template uses {{HDD_PATH}} but hdd_path not set!"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
# Show version override if any
|
|
||||||
local version_override
|
local version_override
|
||||||
version_override=$(yaml_get_override "$customer_file" "${app}_version")
|
version_override=$(yaml_get_override "$customer_file" "${app}_version")
|
||||||
if [[ -n "$version_override" ]]; then
|
if [[ -n "$version_override" ]]; then
|
||||||
echo -e " ${CYAN}[DRY-RUN]${NC} ↳ version pinned to: ${version_override}"
|
echo -e " ${CYAN}[DRY-RUN]${NC} ↳ version pinned to: ${version_override}"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
echo -e " ${CYAN}[DRY-RUN]${NC} Portainer compose paths:"
|
||||||
|
for app in "${apps[@]}"; do
|
||||||
|
echo -e " ${CYAN}[DRY-RUN]${NC} ${customer_id}/${app}/docker-compose.yml"
|
||||||
|
done
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create output directory
|
# Create output directory
|
||||||
mkdir -p "$customer_output"
|
mkdir -p "$customer_output"
|
||||||
|
|
||||||
# Generate a README for the customer repo
|
# Generate a README for this customer's section
|
||||||
cat > "${customer_output}/README.md" << EOF
|
cat > "${customer_output}/README.md" << EOF
|
||||||
# ${customer_id} - Application Stacks
|
# ${customer_id}
|
||||||
|
|
||||||
**Domain:** \`${domain}\`
|
**Domain:** \`${domain}\`
|
||||||
**HDD Path:** \`${hdd_path:-N/A (no HDD apps)}\`
|
**HDD Path:** \`${hdd_path:-N/A}\`
|
||||||
**Generated:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')
|
**Generated:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')
|
||||||
**Source:** felhom-app-catalog (render.sh)
|
|
||||||
|
|
||||||
## Deployed Apps
|
## Deployed Apps
|
||||||
|
|
||||||
| App | Compose Path |
|
| App | Portainer Compose Path |
|
||||||
|-----|-------------|
|
|-----|------------------------|
|
||||||
$(for app in "${apps[@]}"; do echo "| ${app} | \`${app}/docker-compose.yml\` |"; done)
|
$(for app in "${apps[@]}"; do echo "| ${app} | \`${customer_id}/${app}/docker-compose.yml\` |"; done)
|
||||||
|
|
||||||
## Portainer Setup
|
## Portainer Stack Setup
|
||||||
|
|
||||||
For each app, create a stack in Portainer:
|
For each app, create a stack in Portainer:
|
||||||
1. **Stacks → Add Stack → Repository**
|
1. **Stacks → Add Stack → Repository**
|
||||||
2. Repository URL: \`${GITEA_URL}/${GITEA_ORG}/${customer_id}-stacks\`
|
2. Repository URL: \`${repo_display}\`
|
||||||
3. Compose path: \`<appname>/docker-compose.yml\`
|
3. Compose path: \`${customer_id}/<appname>/docker-compose.yml\`
|
||||||
4. Add required environment variables (see comments in compose files)
|
4. Add required environment variables (see comments in compose files)
|
||||||
5. Deploy
|
5. Enable GitOps auto-update (optional, polling interval: 5 min)
|
||||||
|
6. Deploy
|
||||||
|
|
||||||
---
|
---
|
||||||
*Auto-generated by felhom-app-catalog. Do not edit manually.*
|
*Auto-generated by felhom-app-catalog render.sh. Do not edit manually.*
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Process each app
|
# Process each app
|
||||||
@@ -232,7 +243,6 @@ EOF
|
|||||||
rmdir "${customer_output}/${app}" 2>/dev/null || true
|
rmdir "${customer_output}/${app}" 2>/dev/null || true
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
# Remove trailing slash from hdd_path if present
|
|
||||||
local clean_hdd_path="${hdd_path%/}"
|
local clean_hdd_path="${hdd_path%/}"
|
||||||
sed -i "s|{{HDD_PATH}}|${clean_hdd_path}|g" "${customer_output}/${app}/docker-compose.yml"
|
sed -i "s|{{HDD_PATH}}|${clean_hdd_path}|g" "${customer_output}/${app}/docker-compose.yml"
|
||||||
fi
|
fi
|
||||||
@@ -242,20 +252,14 @@ EOF
|
|||||||
version_override=$(yaml_get_override "$customer_file" "${app}_version")
|
version_override=$(yaml_get_override "$customer_file" "${app}_version")
|
||||||
if [[ -n "$version_override" ]]; then
|
if [[ -n "$version_override" ]]; then
|
||||||
log_info " ${app}: applying version override → ${version_override}"
|
log_info " ${app}: applying version override → ${version_override}"
|
||||||
# Find the main image line and replace the tag
|
|
||||||
# This is a simple approach — works for single-image apps and
|
|
||||||
# the first image line in multi-service apps
|
|
||||||
case "$app" in
|
case "$app" in
|
||||||
immich)
|
immich)
|
||||||
# Immich has server + ML images that should match
|
|
||||||
sed -i "s|ghcr.io/immich-app/immich-server:[^ ]*|ghcr.io/immich-app/immich-server:${version_override}|g" \
|
sed -i "s|ghcr.io/immich-app/immich-server:[^ ]*|ghcr.io/immich-app/immich-server:${version_override}|g" \
|
||||||
"${customer_output}/${app}/docker-compose.yml"
|
"${customer_output}/${app}/docker-compose.yml"
|
||||||
sed -i "s|ghcr.io/immich-app/immich-machine-learning:[^ ]*|ghcr.io/immich-app/immich-machine-learning:${version_override}|g" \
|
sed -i "s|ghcr.io/immich-app/immich-machine-learning:[^ ]*|ghcr.io/immich-app/immich-machine-learning:${version_override}|g" \
|
||||||
"${customer_output}/${app}/docker-compose.yml"
|
"${customer_output}/${app}/docker-compose.yml"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
# Generic: replace the first image tag (main service)
|
|
||||||
# Get the first image line and replace its tag
|
|
||||||
local first_image
|
local first_image
|
||||||
first_image=$(grep -m1 "image:" "${customer_output}/${app}/docker-compose.yml" | sed 's/.*image:[[:space:]]*//')
|
first_image=$(grep -m1 "image:" "${customer_output}/${app}/docker-compose.yml" | sed 's/.*image:[[:space:]]*//')
|
||||||
if [[ -n "$first_image" ]]; then
|
if [[ -n "$first_image" ]]; then
|
||||||
@@ -276,40 +280,128 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
# Git push to Gitea
|
# Git push — single monorepo push (all customers together)
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
push_to_gitea() {
|
push_to_gitea() {
|
||||||
local customer_dir="$1"
|
local output_dir="$1"
|
||||||
local customer_id
|
|
||||||
customer_id=$(basename "$customer_dir" | sed 's/-stacks$//')
|
|
||||||
|
|
||||||
local repo_url="${GITEA_URL}/${GITEA_ORG}/${customer_id}-stacks.git"
|
log_step "Pushing to Gitea: ${GITEA_REPO_URL}"
|
||||||
|
|
||||||
log_step "Pushing to Gitea: ${repo_url}"
|
cd "$output_dir"
|
||||||
|
|
||||||
cd "$customer_dir"
|
|
||||||
|
|
||||||
|
# Initialize git if needed, or update remote
|
||||||
if [[ ! -d ".git" ]]; then
|
if [[ ! -d ".git" ]]; then
|
||||||
git init -b main
|
git init -b main
|
||||||
git remote add origin "$repo_url"
|
git remote add origin "$GITEA_REPO_URL"
|
||||||
|
log_info "Initialized new git repo"
|
||||||
|
else
|
||||||
|
# Ensure remote URL is current
|
||||||
|
git remote set-url origin "$GITEA_REPO_URL" 2>/dev/null || \
|
||||||
|
git remote add origin "$GITEA_REPO_URL" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Stage all changes
|
||||||
git add -A
|
git add -A
|
||||||
if git diff --cached --quiet; then
|
|
||||||
log_info "No changes to push for ${customer_id}"
|
# Check if there are changes to commit
|
||||||
|
if git diff --cached --quiet 2>/dev/null; then
|
||||||
|
log_info "No changes to push — output matches Gitea"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
git commit -m "Auto-render: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
# Show what changed
|
||||||
|
local changed_files
|
||||||
if git push origin main 2>/dev/null; then
|
changed_files=$(git diff --cached --name-only | wc -l)
|
||||||
log_success "Pushed to ${repo_url}"
|
log_info "Changed files: ${changed_files}"
|
||||||
else
|
if [[ "$DEBUG" == "true" ]]; then
|
||||||
# Try force push if first time or diverged
|
git diff --cached --name-only | head -20
|
||||||
log_warn "Normal push failed, trying force push..."
|
|
||||||
git push -u origin main --force
|
|
||||||
log_success "Force-pushed to ${repo_url}"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Commit
|
||||||
|
local commit_msg="render: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||||||
|
# Add summary of what customers were rendered
|
||||||
|
local customers_rendered
|
||||||
|
customers_rendered=$(find "$output_dir" -maxdepth 1 -mindepth 1 -type d ! -name '.git' -printf '%f ' 2>/dev/null || true)
|
||||||
|
if [[ -n "$customers_rendered" ]]; then
|
||||||
|
commit_msg="${commit_msg} [${customers_rendered}]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git commit -m "$commit_msg"
|
||||||
|
|
||||||
|
# Push
|
||||||
|
if git push origin main 2>/dev/null; then
|
||||||
|
log_success "Pushed to ${GITEA_REPO_URL}"
|
||||||
|
else
|
||||||
|
log_warn "Normal push failed — trying pull+rebase first..."
|
||||||
|
if git pull --rebase origin main 2>/dev/null; then
|
||||||
|
git push origin main
|
||||||
|
log_success "Rebased and pushed to ${GITEA_REPO_URL}"
|
||||||
|
else
|
||||||
|
log_warn "Rebase failed — force pushing..."
|
||||||
|
git push -u origin main --force
|
||||||
|
log_success "Force-pushed to ${GITEA_REPO_URL}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
# Generate root README for the monorepo
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
generate_root_readme() {
|
||||||
|
local output_dir="$1"
|
||||||
|
shift
|
||||||
|
local customer_files=("$@")
|
||||||
|
|
||||||
|
local repo_display="${GITEA_REPO_URL%.git}"
|
||||||
|
|
||||||
|
cat > "${output_dir}/README.md" << 'HEADER'
|
||||||
|
# Felhom Customer Stacks
|
||||||
|
|
||||||
|
Auto-generated Docker Compose stacks for all Felhom customers.
|
||||||
|
**Do not edit manually** — regenerate with `render.sh` from `felhom-app-catalog`.
|
||||||
|
|
||||||
|
HEADER
|
||||||
|
|
||||||
|
echo "## Customers" >> "${output_dir}/README.md"
|
||||||
|
echo "" >> "${output_dir}/README.md"
|
||||||
|
echo "| Customer | Domain | Hardware | Apps |" >> "${output_dir}/README.md"
|
||||||
|
echo "|----------|--------|----------|------|" >> "${output_dir}/README.md"
|
||||||
|
|
||||||
|
for cf in "${customer_files[@]}"; do
|
||||||
|
local cid cdomain chw capps
|
||||||
|
cid=$(yaml_get_value "$cf" "customer_id")
|
||||||
|
cdomain=$(yaml_get_value "$cf" "domain")
|
||||||
|
chw=$(yaml_get_value "$cf" "hardware")
|
||||||
|
# Count apps
|
||||||
|
capps=$(yaml_get_list "$cf" "apps" | wc -l)
|
||||||
|
echo "| [${cid}](./${cid}/) | \`${cdomain}\` | ${chw} | ${capps} apps |" >> "${output_dir}/README.md"
|
||||||
|
done
|
||||||
|
|
||||||
|
cat >> "${output_dir}/README.md" << EOF
|
||||||
|
|
||||||
|
## Portainer GitOps Setup
|
||||||
|
|
||||||
|
For each app stack on a customer node:
|
||||||
|
1. **Stacks → Add Stack → Repository**
|
||||||
|
2. Repository URL: \`${repo_display}\`
|
||||||
|
3. Compose path: \`<customer-id>/<app>/docker-compose.yml\`
|
||||||
|
4. Set environment variables (secrets — see comments in compose files)
|
||||||
|
5. Optional: Enable auto-update polling (5 min)
|
||||||
|
|
||||||
|
## Regenerating
|
||||||
|
|
||||||
|
From the \`felhom-app-catalog\` repo:
|
||||||
|
\`\`\`bash
|
||||||
|
./scripts/render.sh # Render locally
|
||||||
|
./scripts/render.sh --push # Render + push to Gitea
|
||||||
|
./scripts/render.sh --dry-run # Preview changes
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
---
|
||||||
|
*Last generated: $(date -u '+%Y-%m-%d %H:%M:%S UTC')*
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log_debug "Generated root README.md"
|
||||||
}
|
}
|
||||||
|
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
@@ -323,30 +415,32 @@ OUTPUT_DIR=""
|
|||||||
|
|
||||||
print_help() {
|
print_help() {
|
||||||
cat << 'EOF'
|
cat << 'EOF'
|
||||||
Felhom App Catalog - Customer Repo Renderer v1.0
|
Felhom App Catalog - Customer Repo Renderer v2.0
|
||||||
|
|
||||||
Generates per-customer Docker Compose stacks from templates.
|
Renders Docker Compose stacks from templates into a single Gitea monorepo.
|
||||||
|
All customers share one repo, each in their own subdirectory.
|
||||||
|
|
||||||
USAGE:
|
USAGE:
|
||||||
./render.sh [OPTIONS]
|
./render.sh [OPTIONS]
|
||||||
|
|
||||||
OPTIONS:
|
OPTIONS:
|
||||||
--customer ID Render only this customer
|
--customer ID Render only this customer (others left untouched)
|
||||||
--output-dir DIR Output directory (default: ./output/)
|
--output-dir DIR Output directory (default: ./output/)
|
||||||
--push Push rendered repos to Gitea
|
--push Render + commit + push to Gitea
|
||||||
--dry-run Show what would be done
|
--dry-run Show what would be done
|
||||||
--debug Verbose output
|
--debug Verbose output
|
||||||
-h, --help Show this help
|
-h, --help Show this help
|
||||||
|
|
||||||
ENVIRONMENT VARIABLES:
|
ENVIRONMENT VARIABLES:
|
||||||
GITEA_URL Gitea base URL (default: https://gitea.felhom.eu)
|
GITEA_REPO_URL Full Gitea repo URL
|
||||||
GITEA_ORG Gitea organization (default: customers)
|
(default: https://gitea.dooplex.hu/admin/customers-felhom.eu.git)
|
||||||
|
|
||||||
EXAMPLES:
|
EXAMPLES:
|
||||||
./render.sh # Render all customers
|
./render.sh # Render all customers
|
||||||
./render.sh --customer demo-felhom # Render one customer
|
./render.sh --customer demo-felhom # Render one customer
|
||||||
./render.sh --dry-run # Preview changes
|
./render.sh --dry-run # Preview changes
|
||||||
./render.sh --push # Render + push to Gitea
|
./render.sh --push # Render + push to Gitea
|
||||||
|
GITEA_REPO_URL=https://gitea.example.com/org/repo.git ./render.sh --push
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,13 +463,14 @@ OUTPUT_DIR="${OUTPUT_DIR:-${DEFAULT_OUTPUT_DIR}}"
|
|||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
main() {
|
main() {
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BOLD}Felhom App Catalog — Renderer${NC}"
|
echo -e "${BOLD}Felhom App Catalog — Renderer v2.0${NC}"
|
||||||
echo -e "Templates: ${TEMPLATES_DIR}"
|
echo -e "Templates: ${TEMPLATES_DIR}"
|
||||||
echo -e "Customers: ${CUSTOMERS_DIR}"
|
echo -e "Customers: ${CUSTOMERS_DIR}"
|
||||||
echo -e "Output: ${OUTPUT_DIR}"
|
echo -e "Output: ${OUTPUT_DIR}"
|
||||||
|
echo -e "Gitea repo: ${GITEA_REPO_URL}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Validate directories exist
|
# Validate directories
|
||||||
if [[ ! -d "$TEMPLATES_DIR" ]]; then
|
if [[ ! -d "$TEMPLATES_DIR" ]]; then
|
||||||
log_error "Templates directory not found: $TEMPLATES_DIR"
|
log_error "Templates directory not found: $TEMPLATES_DIR"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -412,35 +507,58 @@ main() {
|
|||||||
mkdir -p "$OUTPUT_DIR"
|
mkdir -p "$OUTPUT_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# If --push and output dir already has a .git from previous push, pull first
|
||||||
|
if [[ "$PUSH" == "true" && "$DRY_RUN" != "true" && -d "${OUTPUT_DIR}/.git" ]]; then
|
||||||
|
log_info "Pulling latest from Gitea before rendering..."
|
||||||
|
cd "$OUTPUT_DIR"
|
||||||
|
git pull --rebase origin main 2>/dev/null || true
|
||||||
|
cd - > /dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
# Render each customer
|
# Render each customer
|
||||||
local success_count=0
|
local success_count=0
|
||||||
for customer_file in "${customer_files[@]}"; do
|
for customer_file in "${customer_files[@]}"; do
|
||||||
if render_customer "$customer_file" "$OUTPUT_DIR"; then
|
if render_customer "$customer_file" "$OUTPUT_DIR"; then
|
||||||
success_count=$((success_count + 1))
|
success_count=$((success_count + 1))
|
||||||
|
|
||||||
# Push to Gitea if requested
|
|
||||||
if [[ "$PUSH" == "true" && "$DRY_RUN" != "true" ]]; then
|
|
||||||
local cid
|
|
||||||
cid=$(yaml_get_value "$customer_file" "customer_id")
|
|
||||||
push_to_gitea "${OUTPUT_DIR}/${cid}-stacks"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Generate root README (only when rendering all customers)
|
||||||
|
if [[ -z "$TARGET_CUSTOMER" && "$DRY_RUN" != "true" ]]; then
|
||||||
|
# Get ALL customer files for the root README (even if we only rendered some)
|
||||||
|
local all_customer_files=()
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
all_customer_files+=("$f")
|
||||||
|
done < <(find "$CUSTOMERS_DIR" -name "*.yaml" -print0 | sort -z)
|
||||||
|
generate_root_readme "$OUTPUT_DIR" "${all_customer_files[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
echo -e "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
echo -e "${BOLD}${GREEN} Rendering complete: ${success_count}/${#customer_files[@]} customers${NC}"
|
echo -e "${BOLD}${GREEN} Rendering complete: ${success_count}/${#customer_files[@]} customers${NC}"
|
||||||
echo -e "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
echo -e "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
echo ""
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" != "true" ]]; then
|
if [[ "$DRY_RUN" != "true" ]]; then
|
||||||
echo -e "${BOLD}Output:${NC}"
|
echo ""
|
||||||
find "$OUTPUT_DIR" -name "docker-compose.yml" -printf " %P\n" | sort
|
echo -e "${BOLD}Output structure:${NC}"
|
||||||
|
# Show tree-like output
|
||||||
|
for d in "${OUTPUT_DIR}"/*/; do
|
||||||
|
[[ -d "$d" && "$(basename "$d")" != ".git" ]] || continue
|
||||||
|
local cid
|
||||||
|
cid=$(basename "$d")
|
||||||
|
local app_count
|
||||||
|
app_count=$(find "$d" -name "docker-compose.yml" | wc -l)
|
||||||
|
echo -e " 📁 ${BOLD}${cid}/${NC} (${app_count} apps)"
|
||||||
|
find "$d" -name "docker-compose.yml" -printf " %P\n" | sort
|
||||||
|
done
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$PUSH" != "true" && "$DRY_RUN" != "true" ]]; then
|
# Push all at once
|
||||||
echo -e "${YELLOW}Tip:${NC} Use --push to automatically push to Gitea repos"
|
if [[ "$PUSH" == "true" && "$DRY_RUN" != "true" ]]; then
|
||||||
|
push_to_gitea "$OUTPUT_DIR"
|
||||||
|
elif [[ "$PUSH" != "true" && "$DRY_RUN" != "true" ]]; then
|
||||||
|
echo -e "${YELLOW}Tip:${NC} Use --push to commit and push to Gitea"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user