diff --git a/JOURNAL.md b/JOURNAL.md index fddaec8..e721eb1 100644 --- a/JOURNAL.md +++ b/JOURNAL.md @@ -1419,6 +1419,57 @@ no logo file ships). --- +### 19. InvenTree (Business-Apps) ✅ +**Date**: 2026-09-06 +**Application**: InvenTree — open-source inventory management (parts, +stock, BOMs, suppliers, purchase/build orders, plugins, REST API). +Upstream: https://github.com/inventree/InvenTree (MIT), v1.5.2. + +**Ticket**: [#658](https://projects.knownelement.com/issues/658) + +**Pattern**: official-image wrapper of `inventree/inventree:1.5.2` +(amd64 digest pinned). Upstream compose splits server (gunicorn :8000) +and worker (`invoke worker`); Cloudron is one container — worker +backgrounded behind the web-port gate (NetBox pattern, third use). + +**Auth gate verdict**: ✅ OIDC preferred. django-allauth in core; +`INVENTREE_SOCIAL_PROVIDERS` (settings.py:1002, JSON) carries the +openid_connect provider; `SOCIALACCOUNT_OPENID_CONNECT_URL_PREFIX=''` +routes SSO at `/accounts/oidc//`. start.sh builds the +provider JSON from `CLOUDRON_OIDC_*` with `server_url` (discovery) + +PKCE. LDAP also in core as an alternative. Local admin seeded with a +generated password persisted at `/app/data/.admin_password`. + +**Key decisions**: + +- Everything env-driven (INVENTREE_DB_*, INVENTREE_CACHE_* = redis + addon with cache DB 1, INVENTREE_SITE_URL, INVENTREE_ADMIN_*). +- `invoke update` driven explicitly on every boot (migrations + + collectstatic); the image's init.sh ENTRYPOINT is bypassed so + ordering is ours. +- Data dir pinned to /app/data — media/static/config.yaml/secret all + persist; the image's own SECRET_KEY_FILE convention then lives in + localstorage too. +- Health endpoint: `/api/system/health/` (upstream's unauthenticated + probe path). +- grind-stack fix: image names must be lowercase — dir basenames are + lowercased for tags/volumes (`grind-InvenTree:test` was invalid). + +**Verification** (grind-stack, ephemeral pg+redis): first boot ~7.5 min +(migrations + static collection) → health 200; home 302→login; +`/accounts/oidc/cloudron/login/` 302s to the issuer (provider JSON +valid); worker gate fired; version-check JSONDecodeError in logs = +release-API fetch noise (sandboxed network), benign. + +**Files Created**: Dockerfile, CloudronManifest.json (port 8000, +localstorage + postgresql + redis, 2048MB, healthCheckPath +/api/system/health/), start.sh, README.md, CHANGELOG.md, +.dockerignore, logo.png (from repo docs/docs/assets). + +**Commit**: `feat: add InvenTree Cloudron package (Business-Apps) [#658]` + +--- + ## Packaging Pattern: Download Pre-Compiled Binaries ### When to Use diff --git a/Package-Workspace/Business-Apps/InvenTree/.dockerignore b/Package-Workspace/Business-Apps/InvenTree/.dockerignore new file mode 100644 index 0000000..f606d5e --- /dev/null +++ b/Package-Workspace/Business-Apps/InvenTree/.dockerignore @@ -0,0 +1 @@ +repo diff --git a/Package-Workspace/Business-Apps/InvenTree/CHANGELOG.md b/Package-Workspace/Business-Apps/InvenTree/CHANGELOG.md new file mode 100644 index 0000000..70a57c9 --- /dev/null +++ b/Package-Workspace/Business-Apps/InvenTree/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog — InvenTree Cloudron Package + +## 1.0.0 — 2026-09-06 + +- Initial package: InvenTree 1.5.2 (official-image wrapper, digest + pinned sha256:2df695b3…) +- postgresql + redis + localstorage addons; gunicorn on :8000 +- INVENTREE_* env mapped from the platform; data dir pinned to + /app/data (media/static/config + persisted secret key) +- OIDC provider JSON built from CLOUDRON_OIDC_* (allauth + openid_connect, PKCE); local admin seeded with generated password +- `invoke update` on boot; RQ worker backgrounded behind web-port gate diff --git a/Package-Workspace/Business-Apps/InvenTree/CloudronManifest.json b/Package-Workspace/Business-Apps/InvenTree/CloudronManifest.json new file mode 100644 index 0000000..9bdc3ba --- /dev/null +++ b/Package-Workspace/Business-Apps/InvenTree/CloudronManifest.json @@ -0,0 +1,23 @@ +{ + "manifestVersion": 2, + "type": "app", + "id": "io.cloudron.inventree", + "title": "InvenTree", + "description": "Open-source inventory management system: parts and stock tracking, bills of materials, suppliers, purchase and build orders, with a plugin ecosystem and REST API. PostgreSQL storage, Redis-backed background workers. SSO via the Cloudron OIDC provider; a local admin account is seeded on first boot (generated password in /app/data/.admin_password).", + "author": "InvenTree contributors", + "website": "https://inventree.org/", + "contactEmail": "cloudron@tsys.dev", + "tagline": "Inventory management: parts, stock, BOMs and orders", + "version": "1.5.2", + "healthCheckPath": "/api/system/health/", + "httpPort": 8000, + "memoryLimit": 2048, + "addons": { + "localstorage": {}, + "postgresql": {}, + "redis": {} + }, + "mediaLinks": [], + "changelog": "Initial Cloudron package for InvenTree 1.5.2 (official-image wrapper of inventree/inventree, digest pinned). start.sh maps the postgresql/redis addons onto INVENTREE_* env, persists secret key + generated admin password under /app/data, builds the django-allauth openid_connect provider JSON from the Cloudron OIDC env, drives `invoke update` (migrations + static) before serving, and backgrounds the RQ worker gated on the web port. Health endpoint /api/system/health/ per upstream convention.", + "icon": "file://logo.png" +} diff --git a/Package-Workspace/Business-Apps/InvenTree/Dockerfile b/Package-Workspace/Business-Apps/InvenTree/Dockerfile new file mode 100644 index 0000000..5655b59 --- /dev/null +++ b/Package-Workspace/Business-Apps/InvenTree/Dockerfile @@ -0,0 +1,39 @@ +# InvenTree Cloudron Package +# +# InvenTree is an open-source inventory management system (Django): +# parts/stock tracking, BOMs, suppliers, purchase orders, build orders, +# plugin ecosystem. PostgreSQL storage + Redis for background-task +# queues (RQ) and optional caching. +# +# Upstream: https://github.com/inventree/InvenTree (MIT), v1.5.2. +# - Official image inventree/inventree (single image; upstream compose +# runs inventree-server [gunicorn :8000] and inventree-worker +# [invoke worker] as separate services - Cloudron is one container, +# so start.sh backgrounds the worker gated on the web port, the +# same pattern as NetBox). +# - Everything is env-configurable via INVENTREE_* (config.md): DB, +# cache, site URL, secret key, admin seed, SOCIAL_PROVIDERS JSON. +# - The image ENTRYPOINT (init.sh) prepares dirs + config template +# and execs the CMD; migrations/static collection run via +# `invoke update`, which start.sh drives explicitly. +# +# Authentication: NATIVE OIDC (preferred) via django-allauth +# (settings.py:1002 reads INVENTREE_SOCIAL_PROVIDERS; +# SOCIALACCOUNT_OPENID_CONNECT_URL_PREFIX='' so SSO routes live at +# /accounts/oidc/...). start.sh builds the provider JSON from the +# Cloudron platform OIDC env (CLOUDRON_OIDC_*). LDAP also exists in +# core (INVENTREE_LDAP_ENABLED) as an alternative. Local login remains; +# the first admin is seeded (generated password, see README). +# +# Pattern: official-image wrapper. Image pinned by tag AND digest +# (amd64 manifest digest of 1.5.2, from docker manifest inspect). +FROM inventree/inventree:1.5.2@sha256:2df695b338b9fbcb4d86a6a3c27c5f53e690d1b58af8a216e69fec49fc178a14 + +# no packages needed: the image ships bash, python, invoke; start.sh +# only orchestrates env + processes +COPY start.sh /app/start.sh + +# gunicorn web server; API/SSO all on this port (worker has no listener) +EXPOSE 8000 + +ENTRYPOINT ["/bin/bash", "/app/start.sh"] diff --git a/Package-Workspace/Business-Apps/InvenTree/README.md b/Package-Workspace/Business-Apps/InvenTree/README.md new file mode 100644 index 0000000..ad3060a --- /dev/null +++ b/Package-Workspace/Business-Apps/InvenTree/README.md @@ -0,0 +1,51 @@ +# InvenTree — Cloudron Package + +[InvenTree](https://github.com/inventree/InvenTree) v1.5.2 — open-source +inventory management: parts and stock, BOMs, suppliers, purchase/build +orders, plugins, REST API. Django + PostgreSQL + Redis(RQ). Packaged as +an **official-image wrapper** around `inventree/inventree:1.5.2` +(digest-pinned). Upstream runs server and worker as separate compose +services; Cloudron is one container, so start.sh backgrounds +`invoke worker` gated on the web port (same pattern as NetBox). + +## Authentication (auth gate: ✅ OIDC preferred) + +django-allauth is built into core; `INVENTREE_SOCIAL_PROVIDERS` +(settings.py:1002) carries provider config. start.sh builds the +`openid_connect` provider from the Cloudron platform env: + +```json +{"openid_connect": {"OAUTH_PKCE_ENABLED": true, "APPS": [{ + "provider_id": "cloudron", "name": "Cloudron SSO", + "server_url": "", "client_id": "...", "secret": "..." +}]}} +``` + +- SSO login route: `https:///accounts/oidc/cloudron/login/` +- LDAP is also available in core (`INVENTREE_LDAP_*`) as an alternative. +- Local login remains for the admin. + +**First-boot admin**: a local `admin` account is seeded with a +generated password stored at `/app/data/.admin_password` (read it via +the Cloudron file manager). Change it after first login, or set +`INVENTREE_ADMIN_EMAIL`/`INVENTREE_ADMIN_PASSWORD` app env vars to +control the seed yourself. + +## Runtime layout + +| Concern | How | +|---------|-----| +| Database | Cloudron `postgresql` addon | +| Cache/queues | Cloudron `redis` addon (cache DB 1; RQ default DB 0) | +| Schema + static | `invoke update` on every boot (migrations + collectstatic) | +| Worker | `invoke worker` backgrounded, gated on web port (post-migration) | +| Persistence | `/app/data` (localstorage): media/, static/, config.yaml, secret key, admin password | +| Health | `/api/system/health/` (upstream's own unauthenticated endpoint) | + +## Notes + +- Version upgrades: bump tag+digest, rebuild — `invoke update` migrates + on first boot. +- The image ENTRYPOINT's venv logic is bypassed (deps are baked in the + image); start.sh controls ordering instead. +- Package pattern + verification: repo `JOURNAL.md`, InvenTree section. diff --git a/Package-Workspace/Business-Apps/InvenTree/logo.png b/Package-Workspace/Business-Apps/InvenTree/logo.png new file mode 100644 index 0000000..e4626e0 Binary files /dev/null and b/Package-Workspace/Business-Apps/InvenTree/logo.png differ diff --git a/Package-Workspace/Business-Apps/InvenTree/start.sh b/Package-Workspace/Business-Apps/InvenTree/start.sh new file mode 100755 index 0000000..1a725f8 --- /dev/null +++ b/Package-Workspace/Business-Apps/InvenTree/start.sh @@ -0,0 +1,97 @@ +#!/bin/bash +set -euo pipefail + +# InvenTree runtime setup for Cloudron: +# 1. map the Cloudron postgresql + redis addons onto INVENTREE_* env +# and pin the data dir into localstorage (/app/data) +# 2. wait for both addons +# 3. drive `invoke update` (migrations + static collection) before +# serving; the image ENTRYPOINT is bypassed so we control ordering +# 4. background the RQ worker (gated on the web port = migrations +# done), then exec gunicorn on :8000 +# +# Authentication: OIDC via the Cloudron platform provider, mapped into +# INVENTREE_SOCIAL_PROVIDERS (django-allauth openid_connect). The first +# admin is seeded via INVENTREE_ADMIN_* with a generated password, +# persisted at /app/data/.admin_password (see README). + +DATA_DIR="/app/data" +ADMIN_PASS_FILE="${DATA_DIR}/.admin_password" + +mkdir -p "${DATA_DIR}" + +# --- 1. Cloudron -> INVENTREE_* environment --------------------------------------- +export INVENTREE_DATA_DIR="${DATA_DIR}" +export INVENTREE_CONFIG_FILE="${DATA_DIR}/config.yaml" + +export INVENTREE_DB_ENGINE=postgresql +export INVENTREE_DB_HOST="${CLOUDRON_POSTGRESQL_HOST:-127.0.0.1}" +export INVENTREE_DB_PORT="${CLOUDRON_POSTGRESQL_PORT:-5432}" +export INVENTREE_DB_NAME="${CLOUDRON_POSTGRESQL_DATABASE:-inventree}" +export INVENTREE_DB_USER="${CLOUDRON_POSTGRESQL_USERNAME:-inventree}" +export INVENTREE_DB_PASSWORD="${CLOUDRON_POSTGRESQL_PASSWORD:-}" + +export INVENTREE_CACHE_ENABLED=true +export INVENTREE_CACHE_HOST="${CLOUDRON_REDIS_HOST:-127.0.0.1}" +export INVENTREE_CACHE_PORT="${CLOUDRON_REDIS_PORT:-6379}" +export INVENTREE_CACHE_PASSWORD="${CLOUDRON_REDIS_PASSWORD:-}" +export INVENTREE_CACHE_DB=1 + +export INVENTREE_SITE_URL="${CLOUDRON_APP_ORIGIN:-http://localhost}" +export INVENTREE_SECRET_KEY="$(cat "${DATA_DIR}/.secret_key" 2>/dev/null || true)" +if [[ -z "${INVENTREE_SECRET_KEY}" ]]; then + ( umask 077; head -c 48 /dev/urandom | od -An -tx1 | tr -d ' \n' > "${DATA_DIR}/.secret_key" ) + export INVENTREE_SECRET_KEY="$(cat "${DATA_DIR}/.secret_key")" + echo "Generated new InvenTree secret key" +fi + +# first admin: generated password, stored under /app/data +if [[ ! -s "${ADMIN_PASS_FILE}" ]]; then + ( umask 077; head -c 12 /dev/urandom | od -An -tx1 | tr -d ' \n' > "${ADMIN_PASS_FILE}" ) + echo "Generated admin password (stored at ${ADMIN_PASS_FILE} - see README)" +fi +export INVENTREE_ADMIN_USER="${INVENTREE_ADMIN_USER:-admin}" +export INVENTREE_ADMIN_PASSWORD="${INVENTREE_ADMIN_PASSWORD:-$(cat "${ADMIN_PASS_FILE}")}" +export INVENTREE_ADMIN_EMAIL="${INVENTREE_ADMIN_EMAIL:-admin@${CLOUDRON_APP_DOMAIN:-localhost}}" + +# platform OIDC -> django-allauth openid_connect (server_url drives +# .well-known discovery; PKCE on) +if [[ -n "${CLOUDRON_OIDC_CLIENT_ID:-}" ]]; then + export INVENTREE_SOCIAL_PROVIDERS="$(jq -nc \ + --arg cid "${CLOUDRON_OIDC_CLIENT_ID}" \ + --arg csec "${CLOUDRON_OIDC_CLIENT_SECRET:-}" \ + --arg issuer "${CLOUDRON_OIDC_ISSUER:-}" \ + '{openid_connect: {OAUTH_PKCE_ENABLED: true, APPS: [{provider_id: "cloudron", name: "Cloudron SSO", server_url: $issuer, client_id: $cid, secret: $csec}]}}')" +fi + +# --- 2. wait for the addons --------------------------------------------------------- +wait_tcp() { + local host="$1" port="$2" name="$3" + echo "Waiting for ${name} at ${host}:${port} ..." + until (exec 3<>"/dev/tcp/${host}/${port}") 2>/dev/null; do + echo "${name} is unavailable - sleeping" + sleep 2 + done + echo "${name} is up" +} + +wait_tcp "${INVENTREE_DB_HOST}" "${INVENTREE_DB_PORT}" "PostgreSQL" +wait_tcp "${INVENTREE_CACHE_HOST}" "${INVENTREE_CACHE_PORT}" "Redis" + +# --- 3. schema + static + plugin setup ---------------------------------------------- +# image layout: INVENTREE_HOME=/home/inventree (tasks.py + gunicorn.conf.py +# + init.sh live here), INVENTREE_BACKEND_DIR=/home/inventree/src/backend +cd "${INVENTREE_HOME:-/home/inventree}" +echo "Running invoke update (migrations + static files) ..." +invoke update + +# --- 4. worker + web server ---------------------------------------------------------- +( + until (exec 3<>/dev/tcp/127.0.0.1/8000) 2>/dev/null; do sleep 2; done + echo "web port is up - starting InvenTree worker" + exec invoke worker +) & + +echo "Starting InvenTree web server on :8000 ..." +exec gunicorn -c ./gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:8000 \ + --chdir "${INVENTREE_BACKEND_DIR:-/home/inventree/src/backend}/InvenTree" diff --git a/README.md b/README.md index 27c6568..cf45ece 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ The Cloudron component focuses on packaging upstream free/libre/open application ### 📊 Current Progress - **Total Applications**: 46 (see [GitUrlList.txt](GitUrlList.txt); pruned 2026-09-06 by human rulings: no-Node policy, k8s redirects, out-of-scope) -- **Completed Packages**: 18/46 (~39% of the post-ruling set) +- **Completed Packages**: 19/46 (~41% of the post-ruling set) - **Packaging Templates**: Created ✅ -- **Packages Committed & Pushed**: 18 ✅ +- **Packages Committed & Pushed**: 19 ✅ - **Build Tickets**: rulings synced 2026-09-06 (#633-#678 batch, umbrella [#632](https://projects.knownelement.com/issues/632), Redmine project 55); grist-core excluded (packaged upstream) @@ -40,6 +40,7 @@ The Cloudron component focuses on packaging upstream free/libre/open application | 16 | NetBox | Infrastructure | ~1GB | 8080 | localstorage, postgresql, redis | ✅ Committed | | 17 | ConsulDemocracy | Collaboration | ~1.6GB | 3000 | localstorage, postgresql | ✅ Committed | | 18 | GoAlert | Monitoring | ~40MB | 8081 | localstorage, postgresql | ✅ Committed | +| 19 | InvenTree | Business-Apps | ~1.4GB | 8000 | localstorage, postgresql, redis | ✅ Committed | ### 📦 Packages in Development diff --git a/STATUS.md b/STATUS.md index 6fb6787..4295b98 100644 --- a/STATUS.md +++ b/STATUS.md @@ -14,14 +14,14 @@ ## Current State: STABLE (packaging phase, ongoing) -Cloudron packaging pipeline is operational. 18 of 46 remaining-set applications are +Cloudron packaging pipeline is operational. 19 of 46 remaining-set applications are packaged, committed, and pushed. Packaging templates exist for the core patterns. The gardening protocol (this file + AGENTS.md) keeps docs in sync. All remaining apps now carry build tickets (#633-#678) under umbrella [#632](https://projects.knownelement.com/issues/632) in Redmine project 55 — ready for the sequential grind-driver pattern. -## Completed Packages (18) +## Completed Packages (19) | # | Application | Category | Pattern | Port(s) | Addons | |---|-------------|----------|---------|---------|--------| @@ -80,7 +80,7 @@ GitUrlList pruned 56 → 46 accordingly. | #655 | Slurm | System-Administration | disposition: OUT OF SCOPE — ruling pending | | #656 | Gophish | Security | blocked-on-auth | | #657 | SniperPhish | Security | upstream gone (404) — ruling pending | -| #658 | InvenTree | Business-Apps | GO: official image, OIDC+LDAP core | +| #658 | InvenTree | Business-Apps | DONE 2026-09-06 (19th package, grind-verified) | | #659 | Mender | System-Administration | build | | #661 | WireViz | Documentation-Tools | disposition: covered by wireviz-web package | | #662 | KillBill | Business-Apps | build | @@ -116,7 +116,7 @@ Full write-ups of each pattern + challenges are in [`JOURNAL.md`](JOURNAL.md). | Low-Code | 2 | 1/2 | corteza done (openblocks removed) | | Monitoring | 5 | 2/5 | healthchecks, goalert done (langfuse removed) | | Automation | 3 | 1/3 | windmill done (huginn removed) | -| Business-Apps | 6 | 1/6 | elabftw done (midday removed) | +| Business-Apps | 6 | 2/6 | elabftw, inventree done (midday removed) | | Collaboration | 2 | 1/2 | consuldemocracy done | | Infrastructure | 6 | 5/6 | easy-gate, rathole, database-gateway, chirpstack, netbox done | | Data-Management | 2 | 0/2 | datahub, seatunnel | @@ -135,7 +135,7 @@ Auth capability is a hard gate before packaging (see LDAP acceptable (risk flag), 🔄 = auth-proxy (no users), ❌ = local-only (unacceptable / blocked-on-auth). -### Completed packages (18) +### Completed packages (19) | App | OIDC | LDAP | Verdict | Note | |-----|------|------|---------|------| @@ -157,6 +157,7 @@ LDAP acceptable (risk flag), 🔄 = auth-proxy (no users), ❌ = local-only | NetBox | yes | plugin | ✅ preferred | **Packaged**; python-social-auth OIDC (`REMOTE_AUTH_BACKEND=OpenIdConnectAuth` + `SOCIAL_AUTH_OIDC_*`) wired from `CLOUDRON_OIDC_*`; local Django login retained for admin bootstrap; SSO users register without privileges (README) | | ConsulDemocracy | yes | no | ✅ preferred | **Packaged**; omniauth_openid_connect (discovery) wired into generated secrets.yml; `feature.oidc_login` auto-enabled on first boot when the provider is present (admin-panel toggle afterwards); SAML also available; local admin seeded | | GoAlert | yes | no | ✅ preferred | **Packaged**; native generic OIDC (go-oidc, any issuer) wired via GOALERT_OIDC_* from the platform provider; new-user creation on; optional first-admin basic auth via GOALERT_ADMIN_USER/PASS; callback /api/v2/identity/providers/oidc/callback | +| InvenTree | yes | yes | ✅ preferred | **Packaged**; django-allauth openid_connect provider JSON built from CLOUDRON_OIDC_* (server_url discovery, PKCE); SSO at /accounts/oidc/cloudron/; LDAP also available; local admin seeded (generated password in /app/data/.admin_password) | ### Candidates researched diff --git a/scripts/grind-stack.sh b/scripts/grind-stack.sh index fb17173..1bd6174 100755 --- a/scripts/grind-stack.sh +++ b/scripts/grind-stack.sh @@ -41,7 +41,7 @@ manifest_field() { jq -r "$1" "$PKG/CloudronManifest.json"; } cmd_up() { PKG="${1:?usage: grind-stack.sh up }" [ -f "$PKG/CloudronManifest.json" ] || die "no CloudronManifest.json in $PKG" - APPNAME="$(basename "$PKG")" + APPNAME="$(basename "$PKG" | tr '[:upper:]' '[:lower:]')" PORT="$(manifest_field .httpPort)" MEM="$(manifest_field .memoryLimit)" ADDONS="$(jq -r '.addons | keys[]' "$PKG/CloudronManifest.json" | sort | tr '\n' ' ')" @@ -121,7 +121,7 @@ cmd_down() { cmd_sweep() { PKG="${1:?usage: grind-stack.sh sweep }" - APPNAME="$(basename "$PKG")" + APPNAME="$(basename "$PKG" | tr '[:upper:]' '[:lower:]')" echo "== grind sweep: $APPNAME" stack_running && die "stack still up - run 'down' before sweep" docker image rm "grind-$APPNAME:test" >/dev/null 2>&1 || echo " (test image already gone)"