Addresses findings from a 6-lens review against the project skills (defining-asset-contract, enforcing-plugin-contract, hardening-flask-config, integrating-plugin-hooks, pinning-flask-behavior, simplifying-python). Security (hardening-flask-config): - Load per-plugin COLLECTOR_API_KEY_<PLUGIN> from env in create_app. from_object only copies class attributes, so per-plugin keys (ADR-006) were dead in real deploys and silently fell back to the shared key. - EMPLOYEE_DB_USER/PASSWORD no longer default to root/rootpassword (no safe default for a secret; unset fails loud). Documented in .env.example + DEPLOY.md. - COLLECTOR_API_KEY + per-plugin + EMPLOYEE_DB_* added to .env.example/DEPLOY.md. Hook isolation (integrating-plugin-hooks): - collector _collector_plugins and dashboard get_navigation now re-raise in dev/test and log+isolate in prod, instead of silently swallowing a broken plugin hook. Plugin loader (enforcing-plugin-contract): - enable_plugin/install_plugin read dependencies+version from the manifest instead of instantiating the plugin class. - _register_plugin_components rejects a second plugin claiming an already-used api_prefix (reset per app in init_app). Tests (pinning-flask-behavior): - test_identifiers.py: gauge/maintenance round-trip on computer/printer/network create+update; per-type seed yields the 12 identifier keys. - contract tests for apply_collector_payload presence + schema-declarers-implement. - security tests for per-plugin key env loading + no employee-db password default. Docs/contract sync (defining-asset-contract): - PLUGIN-HOOKS.md documents apply_collector_payload; stale 0.2.0 -> 0.3.0. - ADR-006 documents apply_collector_payload + single-dispatch rationale. - ADR-001 enumerates the expanded shopdb.api import surface. Simplify (simplifying-python): - De-duplicate the 21-entry settings defaults: shared build_default_settings() used by both the /settings/seed route and the CLI (were drifting copies). - Remove dead AssetStatus import + redundant AssetType local import in computers plugin; comment the statusid=1 collector default. 153 tests pass (was 145), naming/style green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.5 KiB
Per-Site Deployment Runbook
shopdb-flask is single-tenant per ADR-004. Each adopting facility runs its own stack: own DB, own users, own enabled plugins, own secrets. This document is the runbook for a fresh site deploy.
Prerequisites
- Docker 24+ and Docker Compose v2 (or equivalent container runtime)
- A reverse proxy with TLS termination (nginx, traefik, Caddy, GE corporate LB) -- the framework does not terminate TLS itself
- A MySQL backup destination (offsite recommended)
- Access to the GE Aerospace Gitea or a clone of the repo
Step 1: Clone and configure
git clone https://gitea.proudtech.net/ge-aerospace/shopdb-flask.git
cd shopdb-flask
cp .env.example .env
Edit .env:
| Variable | Required | Notes |
|---|---|---|
FLASK_ENV |
Yes | production for live sites |
SECRET_KEY |
Yes | python -c "import secrets; print(secrets.token_urlsafe(64))" |
JWT_SECRET_KEY |
Yes | Same generation, different value |
DATABASE_URL |
Yes | mysql+pymysql://shopdb:PASSWORD@db:3306/shopdb_flask (matches docker-compose) |
CORS_ORIGINS |
Yes | Comma-separated explicit origins. Wildcard rejected. |
MYSQL_ROOT_PASSWORD |
Yes | Container only |
MYSQL_PASSWORD |
Yes | Container only, must match DATABASE_URL password |
MYSQL_PORT |
No | Default 3306 |
API_PORT |
No | Default 5001 |
LOG_LEVEL |
No | Default INFO |
ZABBIX_URL, ZABBIX_TOKEN |
No | Only if printers plugin uses Zabbix |
COLLECTOR_API_KEY |
No | Shared key for /api/collector/* ingest. Required only if unattended collectors push data. Endpoint fails closed (denies) when unset. |
COLLECTOR_API_KEY_<PLUGIN> |
No | Per-plugin override (e.g. COLLECTOR_API_KEY_COMPUTERS), checked before the shared key (ADR-006) |
EMPLOYEE_DB_HOST/USER/PASSWORD/NAME |
No | Read-only HR directory for notifications + kiosks. No safe default for the password. |
Step 2: Bring up the stack
docker compose build
docker compose up -d
The MySQL container initializes its volume on first run. The API container waits for db to be healthy via healthcheck. Check logs:
docker compose logs -f api
If ProductionConfig.validate() raises, the container exits with the offending env-var named in the log. Fix .env and docker compose up -d again.
Step 3: Initialize the database schema
docker compose exec api flask db upgrade
This applies the baseline migration (creates all tables) and any subsequent migrations. Re-running is idempotent.
Step 4: Seed reference data
docker compose exec api flask seed reference-data
Creates: default Vendor, Location, BusinessUnit, OperatingSystem, AssetStatus, RelationshipType rows seeded with the platform contract values (partof, controls, connectedto).
Step 5: Pick plugins to enable
The image bundles all six plugins (computers, equipment, network, notifications, printers, usb). Only enabled plugins are loaded.
docker compose exec api flask plugin list
docker compose exec api flask plugin install computers
docker compose exec api flask plugin install equipment
# ... repeat for each plugin the site tracks
To install a sister-site or third-party plugin (per ADR-003), drop its directory into <repo>/plugins/<name>/ (the docker-compose mounts this read-only into the container) and run flask plugin install <name>.
Step 6: Create the admin user
docker compose exec api flask seed admin --username admin --email admin@facility.example.com
# Password is generated and printed once. Store in your password manager.
Subsequent users are managed through the UI.
Step 7: Front the API with TLS
The Flask container listens on 5001/tcp over plain HTTP. Production exposure must go through a reverse proxy that terminates TLS:
server {
listen 443 ssl;
server_name shopdb.facility-a.example.com;
ssl_certificate /etc/ssl/certs/shopdb.crt;
ssl_certificate_key /etc/ssl/private/shopdb.key;
location / {
proxy_pass http://localhost:5001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
The framework reads X-Forwarded-For for audit logging.
Step 8: Backups
Per-site MySQL backups are the site's responsibility. Recommended: nightly mysqldump to offsite storage with 14-day retention.
docker compose exec -T db mysqldump -u root -p"${MYSQL_ROOT_PASSWORD}" shopdb_flask | gzip > backup-$(date +%F).sql.gz
Verify a restore quarterly.
Step 9: Updates
git pull origin main
docker compose build api
docker compose up -d api
docker compose exec api flask db upgrade
The framework's __contract_version__ may have moved. Check docs/adr/ for any new ADRs since the last update. If an ADR introduces a breaking change, the upgrade may require coordinated work; the ADR's "Consequences" section documents it.
Common issues
| Symptom | Cause | Fix |
|---|---|---|
ConfigError: SECRET_KEY is required in production |
.env missing or blank |
Set SECRET_KEY in .env, re-up |
ConfigError: CORS_ORIGINS must be a comma-separated allowlist |
.env has * |
Set explicit origins |
PluginVersionError: requires core_version X but framework is Y |
Plugin pinned a too-narrow range | Update manifest.json core_version or pin framework version |
500s after flask db upgrade |
Migration ran but app cached old schema | docker compose restart api |
| Cannot reach API after restart | Reverse proxy not pointing at the container's exposed port | Confirm API_PORT and proxy config |
Health check
curl -s -X POST -H "Content-Type: application/json" \
-d '{}' http://localhost:5001/api/auth/login \
| jq .
# Expect: {"status": "error", "data": {"error": {"code": "VALIDATION_ERROR", ...}}}
If this returns a 500 or no JSON, the container is unhealthy. Check docker compose logs api.
References
- docs/adr/ADR-004-deployment-topology.md - per-site instances rationale
- docs/adr/ADR-003-plugin-distribution.md - bundled vs external plugins
- docs/adr/ADR-006-collector-contract.md - per-plugin collector endpoints
- docs/PLUGIN-QUICKSTART.md - building a custom plugin for your site
- shopdb/config.py - all the env-vars in one place