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>
165 lines
6.5 KiB
Markdown
165 lines
6.5 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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.
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```nginx
|
|
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.
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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](adr/ADR-004-deployment-topology.md) - per-site instances rationale
|
|
- [docs/adr/ADR-003-plugin-distribution.md](adr/ADR-003-plugin-distribution.md) - bundled vs external plugins
|
|
- [docs/adr/ADR-006-collector-contract.md](adr/ADR-006-collector-contract.md) - per-plugin collector endpoints
|
|
- [docs/PLUGIN-QUICKSTART.md](PLUGIN-QUICKSTART.md) - building a custom plugin for your site
|
|
- [shopdb/config.py](../shopdb/config.py) - all the env-vars in one place
|