Jun 27 Update documents
This commit is contained in:
+158
-38
@@ -1,84 +1,204 @@
|
||||
# Control Plane (MT-0)
|
||||
# Control Plane (MT-0 → MT-3)
|
||||
|
||||
Tenant registry, plans, domains, provisioning state, and superadmin accounts
|
||||
for multi-tenant JQC. Self-contained and decoupled from `app/` — the existing
|
||||
single-tenant application is unaffected by this package.
|
||||
for multi-tenant JQC. Self-contained and decoupled from `app/` — imports
|
||||
nothing from the data-plane application.
|
||||
|
||||
See `../MULTI_TENANT_PLAN.md` for the full architecture and roadmap.
|
||||
See `../MULTI_TENANT_PLAN.md` for the full phased roadmap.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
control/
|
||||
├── __init__.py # package docs
|
||||
├── base.py # ControlBase + engine/session (from CONTROL_DATABASE_URL)
|
||||
├── crypto.py # Fernet encrypt/decrypt for tenant DB passwords
|
||||
├── base.py # ControlBase + engine/session (CONTROL_DATABASE_URL)
|
||||
├── crypto.py # Fernet encrypt/decrypt for tenant DB passwords (CONTROL_FERNET_KEY)
|
||||
├── time_utils.py # now_eastern() mirror (no app import)
|
||||
├── models.py # Plan, PlanFeature, Tenant, TenantDomain,
|
||||
│ # Superadmin, ProvisioningJob, TenantAudit
|
||||
├── seed.py # idempotent baseline-plan seeder
|
||||
│ # Superadmin, ProvisioningJob, TenantAudit
|
||||
├── seed.py # idempotent baseline-plan seeder (Free/Starter/Pro/Enterprise)
|
||||
├── cli.py # seed / create-superadmin / list-plans
|
||||
├── tenant_migrate.py # MT-2 — upgrade_tenant / bootstrap_tenant / chain_head + CLI
|
||||
├── provision.py # MT-3 — create_tenant / register_tenant_zero / delete_tenant + CLI
|
||||
└── migrations/ # standalone Alembic chain (control{N}_…)
|
||||
└── versions/control0001_init.py ← HEAD
|
||||
└── versions/
|
||||
└── control0001_init.py ← HEAD (7 tables)
|
||||
```
|
||||
|
||||
## Environment variables (control plane only)
|
||||
## Environment variables
|
||||
|
||||
All must live in `/etc/jqc/control.env` (see §3 below). Never hard-code.
|
||||
|
||||
| Variable | Purpose |
|
||||
|---|---|
|
||||
| `CONTROL_DATABASE_URL` | e.g. `mysql+pymysql://jqc_control:pw@localhost/jqc_control` |
|
||||
| `CONTROL_FERNET_KEY` | Fernet key for encrypting tenant DB passwords |
|
||||
| `CONTROL_DATABASE_URL` | e.g. `mysql+pymysql://jqc_control:pw@127.0.0.1/jqc_control` |
|
||||
| `CONTROL_FERNET_KEY` | Fernet key for encrypting tenant DB passwords. Generate once, never rotate without re-encrypting all tenant rows. |
|
||||
| `PROVISION_DB_URL` | MySQL account with `CREATE DATABASE` / `CREATE USER` / `GRANT` rights. |
|
||||
| `TENANT_BASE_DOMAIN` | Apex for subdomains, e.g. `jqc.app`. Builds `<slug>.jqc.app`. |
|
||||
|
||||
Generate a Fernet key:
|
||||
Generate a Fernet key (run once, store permanently):
|
||||
|
||||
```bash
|
||||
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
||||
```
|
||||
|
||||
## Deploy — MT-0 bootstrap (run once)
|
||||
## Canonical env file (required — prevents Fernet key mismatch)
|
||||
|
||||
Migrations are independent of the tenant chain and the data plane. The existing
|
||||
app does **not** need to be touched or restarted for MT-0.
|
||||
The `CONTROL_FERNET_KEY` used at provisioning time must be identical to the key
|
||||
the running app uses to decrypt tenant creds. A mismatch causes
|
||||
`cryptography.fernet.InvalidToken` at request time. One file prevents this:
|
||||
|
||||
```bash
|
||||
# 1. Create the control database + its MySQL user (run as a MySQL admin)
|
||||
mysql -e "CREATE DATABASE jqc_control CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||||
mysql -e "CREATE USER 'jqc_control'@'localhost' IDENTIFIED BY '<pw>';"
|
||||
mysql -e "GRANT ALL PRIVILEGES ON jqc_control.* TO 'jqc_control'@'localhost'; FLUSH PRIVILEGES;"
|
||||
# /etc/jqc/control.env (chmod 640, chown root:jqc)
|
||||
CONTROL_DATABASE_URL=mysql+pymysql://jqc_control:<pw>@127.0.0.1/jqc_control
|
||||
CONTROL_FERNET_KEY=<key — no quotes, no trailing space>
|
||||
PROVISION_DB_URL=mysql+pymysql://jqc_provisioner:<pw>@127.0.0.1/
|
||||
TENANT_BASE_DOMAIN=jqc.app
|
||||
MULTI_TENANT_ENABLED=true
|
||||
```
|
||||
|
||||
# 2. Export the env vars (add to .env or the systemd unit for the control panel later)
|
||||
export CONTROL_DATABASE_URL='mysql+pymysql://jqc_control:<pw>@localhost/jqc_control'
|
||||
export CONTROL_FERNET_KEY='<generated key>'
|
||||
```ini
|
||||
# systemd unit [Service]
|
||||
EnvironmentFile=/etc/jqc/control.env
|
||||
```
|
||||
|
||||
# 3. Apply the control schema
|
||||
```bash
|
||||
# any CLI session that provisions tenants
|
||||
set -a; . /etc/jqc/control.env; set +a
|
||||
```
|
||||
|
||||
## MySQL accounts required
|
||||
|
||||
```sql
|
||||
-- Control DB user
|
||||
CREATE DATABASE jqc_control CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE USER 'jqc_control'@'localhost' IDENTIFIED BY '<pw>';
|
||||
GRANT ALL PRIVILEGES ON jqc_control.* TO 'jqc_control'@'localhost'; FLUSH PRIVILEGES;
|
||||
|
||||
-- Provisioner (creates per-tenant DBs + users)
|
||||
CREATE USER 'jqc_provisioner'@'localhost' IDENTIFIED BY '<pw>';
|
||||
GRANT ALL PRIVILEGES ON *.* TO 'jqc_provisioner'@'localhost' WITH GRANT OPTION;
|
||||
GRANT CREATE USER ON *.* TO 'jqc_provisioner'@'localhost'; FLUSH PRIVILEGES;
|
||||
```
|
||||
|
||||
## Bootstrap — MT-0 (run once per environment)
|
||||
|
||||
```bash
|
||||
set -a; . /etc/jqc/control.env; set +a
|
||||
|
||||
# 1. Apply control schema (7 tables)
|
||||
alembic -c control/migrations/alembic.ini upgrade head
|
||||
|
||||
# 4. Seed baseline plans (Free / Starter / Pro / Enterprise)
|
||||
# 2. Seed Free / Starter / Pro / Enterprise plans
|
||||
python -m control.cli seed
|
||||
|
||||
# 5. Create the first superadmin
|
||||
# 3. First superadmin
|
||||
python -m control.cli create-superadmin --username admin --email you@example.com
|
||||
|
||||
# Verify
|
||||
alembic -c control/migrations/alembic.ini current # → control0001_init (head)
|
||||
python -m control.cli list-plans # → 4 plans
|
||||
```
|
||||
|
||||
Verify:
|
||||
## Tenant-zero — register existing LT in place (MT-3)
|
||||
|
||||
No DB creation, no data movement, no schema changes to the live LT database.
|
||||
|
||||
```bash
|
||||
alembic -c control/migrations/alembic.ini current # → control0001_init (head)
|
||||
python -m control.cli list-plans
|
||||
python -m control.provision register-tenant-zero \
|
||||
--slug lts --name "LT Services" --plan enterprise \
|
||||
--db-host 127.0.0.1 \
|
||||
--db-name <LT_DATABASE_NAME> \
|
||||
--db-user <LT_DB_USER> \
|
||||
--db-password '<LT_DB_PASSWORD>' \
|
||||
--custom-domain jqc.ltservicesinc.com \
|
||||
--base-domain jqc.app
|
||||
```
|
||||
|
||||
Output includes `alembic_head` (read from the existing `alembic_version` table)
|
||||
and both domain mappings (`lts.jqc.app` + `jqc.ltservicesinc.com` as verified).
|
||||
|
||||
## Provisioning a new tenant (MT-3)
|
||||
|
||||
```bash
|
||||
python -m control.provision create-tenant \
|
||||
--slug acme \
|
||||
--name "Acme Corp" \
|
||||
--plan pro \
|
||||
--admin-email ops@acme.com \
|
||||
--base-domain jqc.app
|
||||
```
|
||||
|
||||
Steps executed automatically:
|
||||
1. `CREATE DATABASE jqc_acme` + `CREATE USER jqc_acme_u` + `GRANT` (scoped to that DB)
|
||||
2. Insert `tenants` row with Fernet-encrypted password
|
||||
3. `bootstrap_tenant()` → baseline schema + `stamp head` (phase migrations skipped)
|
||||
4. Insert first admin into tenant DB with a set-password token
|
||||
5. Register `acme.jqc.app` (verified) + optional custom domain (unverified)
|
||||
6. Set tenant `status = active`
|
||||
|
||||
Output includes the **admin setup link** — send to the admin's email:
|
||||
`https://acme.jqc.app/customers/set-password/<token>`
|
||||
|
||||
On failure, a best-effort rollback drops the DB/user and removes the tenant row
|
||||
so a retry starts clean.
|
||||
|
||||
## Per-tenant migrations (MT-2)
|
||||
|
||||
```bash
|
||||
# New fresh DB (empty → full schema + stamp head; phase migrations skipped)
|
||||
python -m control.tenant_migrate bootstrap --tenant acme
|
||||
|
||||
# Incremental upgrade (phase33+ — must be INFORMATION_SCHEMA-guarded)
|
||||
python -m control.tenant_migrate upgrade --tenant all
|
||||
python -m control.tenant_migrate upgrade --tenant acme # by slug
|
||||
python -m control.tenant_migrate upgrade --tenant 3 # by id
|
||||
|
||||
# Status check
|
||||
python -m control.tenant_migrate current --tenant all
|
||||
python -m control.tenant_migrate heads
|
||||
```
|
||||
|
||||
**Rule:** every new migration (phase33+) MUST use INFORMATION_SCHEMA existence
|
||||
checks (`_table_exists`, `_column_exists`, `_index_exists`) — the same pattern
|
||||
used by the squashed baseline. Unguarded migrations break idempotency and cannot
|
||||
be re-run safely across the fleet.
|
||||
|
||||
## Delete / deregister a tenant
|
||||
|
||||
```bash
|
||||
# Provisioned tenant — delete DB + control records
|
||||
python -m control.provision delete-tenant --slug acme --drop-db --yes
|
||||
|
||||
# Adopted DB (tenant-zero) — remove control records only; NEVER --drop-db
|
||||
python -m control.provision delete-tenant --slug lts --yes
|
||||
```
|
||||
|
||||
`--drop-db` is guarded: it only proceeds when the stored `db_name` matches the
|
||||
provisioner convention (`jqc_<slug>`). This prevents accidental drops of
|
||||
externally-named databases like LT's production DB.
|
||||
|
||||
## Rollback
|
||||
|
||||
```bash
|
||||
alembic -c control/migrations/alembic.ini downgrade base # drops all control tables
|
||||
# Drop all 7 control tables (destructive)
|
||||
alembic -c control/migrations/alembic.ini downgrade base
|
||||
```
|
||||
|
||||
## Notes
|
||||
## Plan tier matrix
|
||||
|
||||
- The migration uses INFORMATION_SCHEMA existence checks (Rule 14) — safe to re-run.
|
||||
- Plan seeding is idempotent (upsert by `code`) — re-running updates in place.
|
||||
- Tenant DB passwords are stored Fernet-encrypted in `tenants.db_password_enc`;
|
||||
`Tenant.db_uri` decrypts on demand. Provisioning that *creates* per-tenant
|
||||
MySQL users/grants lands in MT-3.
|
||||
- Control-panel write auditing (`tenant_audit`) is wired in MT-4; the bootstrap
|
||||
CLI logs to stdout only.
|
||||
| Axis | Free | Starter | Pro | Enterprise |
|
||||
|---|---|---|---|---|
|
||||
| Max users | 3 | 15 | 50 | unlimited |
|
||||
| Max facilities | 2 | 10 | 50 | unlimited |
|
||||
| Inspections / month | 50 | 500 | 5 000 | unlimited |
|
||||
| Issues / month | 50 | 500 | 5 000 | unlimited |
|
||||
| Mobile API | ✗ | ✓ | ✓ | ✓ |
|
||||
| Scheduled reports | ✗ | ✗ | ✓ | ✓ |
|
||||
| Branding | ✗ | ✗ | ✓ | ✓ |
|
||||
| Custom domain | ✗ | ✗ | ✓ | ✓ |
|
||||
| Subdomain | ✓ | ✓ | ✓ | ✓ |
|
||||
|
||||
Quota-exceed: **soft warn** — allow submit, flag for upgrade, never reject.
|
||||
Plans are seeded idempotently; re-running `python -m control.cli seed` updates
|
||||
in-place without creating duplicates.
|
||||
|
||||
Reference in New Issue
Block a user