Skip to content

Commit 10c2065

Browse files
committed
feat: add ops schema reset functionality and update documentation
- Introduced a new script `run-ops-db-schema-reset-k8s.sh` to facilitate a full database schema reset in Kubernetes, addressing checksum mismatches. - Added a new suspended CronJob `metaboost-db-drop-everything` to drop the public schema on both app and management databases. - Updated documentation in `DB-MIGRATIONS.md` and `REMOTE-K8S-GITOPS.md` to include detailed instructions for the ops schema reset process. - Enhanced the `rebootstrap-full-bootstrap.sh` script to create the `uuid-ossp` extension during rebootstrap. - Updated `kustomization.yaml` to include the new CronJob resource.
1 parent 3f8fd1f commit 10c2065

10 files changed

Lines changed: 214 additions & 4 deletions

File tree

.cursor/skills/linear-db-migrations/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Treat each chain as **ordered fresh applies**: migration `NNNN` may assume schem
3232
## Runner and validation
3333

3434
- Apply migrations: `bash scripts/database/run-linear-migrations.sh --database app|management` (always pass `--database`; there is no default).
35+
- **K8s schema reset (checksum mismatch):** `bash scripts/database/run-ops-db-schema-reset-k8s.sh` or `npm run db:ops:schema-reset:k8s` (requires `K8S_NAMESPACE`; runs drop → rebootstrap → migrate → verify → superuser-create jobs).
3536
- **Credentials:** **app** migrations use `DB_APP_MIGRATOR_USER`, `DB_APP_MIGRATOR_PASSWORD`, `DB_APP_NAME`, `DB_HOST`, and `DB_PORT`. **Management** migrations use `DB_MANAGEMENT_MIGRATOR_USER`, `DB_MANAGEMENT_MIGRATOR_PASSWORD`, `DB_MANAGEMENT_NAME`, `DB_HOST`, and `DB_PORT`. Optional: `infra/config/local/db.env` when keys are unset before sourcing.
3637
- K8s wrapper: `bash scripts/database/run-linear-migrations-k8s.sh` (`--database` required); validates the same keys from Secrets.
3738
- Validate: `bash scripts/database/validate-linear-migrations.sh` (and `--check-db` to compare on-disk checksums to `linear_migration_history` when a DB is available).
@@ -44,7 +45,7 @@ Treat each chain as **ordered fresh applies**: migration `NNNN` may assume schem
4445
## Ops bundle (cache busting)
4546

4647
- `infra/k8s/base/ops/kustomization.yaml` must list every `.sql` file under the app and management `source` directories so the ops jobs ConfigMaps stay in sync.
47-
- The ops migration-runtime ConfigMap also bundles `verify-bootstrap-contract.sh` and `rebootstrap-full-bootstrap.sh` for suspended CronJobs `metaboost-db-verify-bootstrap-contract` and `metaboost-db-rebootstrap-roles`.
48+
- The ops migration-runtime ConfigMap also bundles `verify-bootstrap-contract.sh` and `rebootstrap-full-bootstrap.sh` for suspended CronJobs `metaboost-db-verify-bootstrap-contract`, `metaboost-db-rebootstrap-roles`, and `metaboost-db-drop-everything`.
4849
- Kustomize may load paths outside the ops directory; when building, use e.g. `kubectl kustomize infra/k8s/base/ops --load-restrictor LoadRestrictionsNone`.
4950
- Local operator check: `make db_verify_bootstrap_contract` (wraps `scripts/database/verify-bootstrap-contract.sh`).
5051

docs/development/DB-MIGRATIONS.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ npm run db:migrate:linear:dry-run:management
7070

7171
Suspended CronJobs:
7272

73+
- `metaboost-db-drop-everything` — drops `public` schema on app and management DBs (destructive)
7374
- `metaboost-db-migrate-app`
7475
- `metaboost-db-migrate-management`
7576
- `metaboost-db-verify-bootstrap-contract`
@@ -79,7 +80,44 @@ Suspended CronJobs:
7980

8081
Trigger one-off jobs during first deploy and any deploy that introduces new migration files. After schema or role recovery, run verify-bootstrap-contract or rebootstrap-roles manually before app rollout.
8182

82-
Example on-demand triggers:
83+
### Ops-only schema reset (checksum mismatch / edited historical SQL)
84+
85+
When a database already applied older migration file contents and a new release changes those files
86+
(checksum mismatch in migrate job logs), **do not** edit `linear_migration_history` checksums by hand.
87+
For disposable environments (e.g. alpha), reset schema via ops jobs only:
88+
89+
1. `metaboost-db-drop-everything`
90+
2. `metaboost-db-rebootstrap-roles`
91+
3. `metaboost-db-migrate-app`
92+
4. `metaboost-db-migrate-management`
93+
5. `metaboost-db-verify-bootstrap-contract`
94+
6. `metaboost-management-superuser-create`
95+
96+
Then rollout-restart API workloads. Scale down app tiers first if you want a quiet cutover.
97+
98+
From repo root (waits for each job):
99+
100+
```bash
101+
export K8S_NAMESPACE=metaboost-alpha
102+
bash scripts/database/run-ops-db-schema-reset-k8s.sh
103+
```
104+
105+
Or trigger each CronJob manually (Argo CD UI or kubectl):
106+
107+
```bash
108+
kubectl -n <namespace> create job --from=cronjob/metaboost-db-drop-everything metaboost-db-drop-everything-manual-$(date +%s)
109+
kubectl -n <namespace> create job --from=cronjob/metaboost-db-rebootstrap-roles metaboost-db-rebootstrap-roles-manual-$(date +%s)
110+
kubectl -n <namespace> create job --from=cronjob/metaboost-db-migrate-app metaboost-db-migrate-app-manual-$(date +%s)
111+
kubectl -n <namespace> create job --from=cronjob/metaboost-db-migrate-management metaboost-db-migrate-management-manual-$(date +%s)
112+
kubectl -n <namespace> create job --from=cronjob/metaboost-db-verify-bootstrap-contract metaboost-db-verify-bootstrap-contract-manual-$(date +%s)
113+
K8S_NAMESPACE=<namespace> npm run management:superuser:create:k8s
114+
```
115+
116+
**PVC wipe** (empty volume + docker-entrypoint init baselines) is an alternative; you do **not** need
117+
drop/rebootstrap when first-start init runs successfully. See
118+
[REMOTE-K8S-POSTGRES-REINIT.md](/docs/development/k8s/REMOTE-K8S-POSTGRES-REINIT.md).
119+
120+
Example on-demand triggers (individual jobs):
83121

84122
```bash
85123
K8S_NAMESPACE=<namespace> npm run management:superuser:create:k8s

docs/development/k8s/REMOTE-K8S-GITOPS.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,52 @@ kubectl -n "$NAMESPACE" get svc,ingress
218218
kubectl -n argocd get applications
219219
```
220220

221+
### 9. Post-sync DB bootstrap verification (required)
222+
223+
After **db** sync on a **new empty volume**, or after the **ops schema-reset** job sequence (see §10),
224+
verify extension, grants, and `linear_migration_history` with the suspended ops CronJob:
225+
226+
```fish
227+
kubectl -n $NAMESPACE create job --from=cronjob/metaboost-db-verify-bootstrap-contract metaboost-db-verify-bootstrap-contract-manual
228+
kubectl -n $NAMESPACE logs -f job/metaboost-db-verify-bootstrap-contract-manual
229+
```
230+
231+
Expected result includes: `Bootstrap contract verification passed for app and management databases.`
232+
233+
If it fails, do **not** proceed to app rollouts until the DB bootstrap contract is green.
234+
235+
### 10. Ops-only schema reset (checksum mismatch / edited historical SQL)
236+
237+
When migrate jobs fail with **checksum mismatch for already-applied migration** (common after a
238+
release edits `0001_*` or other files that an environment already applied), reset via **ops CronJobs**
239+
only — no PVC delete required. **Destructive:** drops `public` schema on app and management DBs.
240+
241+
**Prerequisite:** `metaboost-<env>-ops` synced so `metaboost-db-drop-everything` exists (Metaboost
242+
`base/ops` at your pinned `?ref=`).
243+
244+
Run jobs **in order** (Argo CD UI: create Job from each suspended CronJob, or from Metaboost repo root):
245+
246+
1. `metaboost-db-drop-everything`
247+
2. `metaboost-db-rebootstrap-roles`
248+
3. `metaboost-db-migrate-app`
249+
4. `metaboost-db-migrate-management`
250+
5. `metaboost-db-verify-bootstrap-contract` (§9)
251+
6. `metaboost-management-superuser-create`
252+
253+
One-shot from Metaboost monorepo (waits for each step):
254+
255+
```bash
256+
export K8S_NAMESPACE=metaboost-alpha
257+
npm run db:ops:schema-reset:k8s
258+
```
259+
260+
Then rollout-restart API deployments and re-sync web tiers. Full detail:
261+
[DB-MIGRATIONS.md](/docs/development/DB-MIGRATIONS.md).
262+
263+
**PVC wipe alternative:** delete Postgres PVC and rely on first-start init baselines — see
264+
[REMOTE-K8S-POSTGRES-REINIT.md](/docs/development/k8s/REMOTE-K8S-POSTGRES-REINIT.md) §3. You do **not**
265+
need drop/rebootstrap when that init path completes successfully.
266+
221267
## GitOps overlay contract
222268

223269
- Every deployed overlay uses immutable refs for remote Metaboost bases.

docs/development/k8s/REMOTE-K8S-POSTGRES-REINIT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Replace **`metaboost-alpha`** with your namespace if different.
1212

1313
**Migrating from the old Deployment layout:** If the namespace still has PVC **`metaboost-postgres-data`** (Deployment-era), replacing it with this StatefulSet creates a **new** PVC **`db-data-metaboost-db-0`**. Back up data first, or delete the old Deployment/PVC when a wipe is acceptable so the new pod initializes cleanly.
1414

15-
**Existing data / drift / password rotation without wipe:** Use **§4** (manual SQL from your machine) or delete the Postgres PVC and bring the pod back so **`PGDATA`** is empty and first-start init runs again (**§3**).
15+
**Existing data / drift / password rotation without wipe:** Use **§4** (manual SQL from your machine), the **ops schema-reset job sequence** in [DB-MIGRATIONS.md](/docs/development/DB-MIGRATIONS.md) (drop → rebootstrap → migrate → verify → superuser), or delete the Postgres PVC and bring the pod back so **`PGDATA`** is empty and first-start init runs again (**§3**).
1616

1717
---
1818

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Pattern: Suspended CronJob (Manual Trigger Only)
2+
# DANGER: Drops public schema on both app and management logical databases. Only runs when explicitly triggered.
3+
# Recovery sequence: drop-everything → rebootstrap-roles → migrate-app → migrate-management →
4+
# verify-bootstrap-contract → management-superuser-create. See docs/development/DB-MIGRATIONS.md.
5+
apiVersion: batch/v1
6+
kind: CronJob
7+
metadata:
8+
name: metaboost-db-drop-everything
9+
spec:
10+
schedule: "0 0 1 1 *"
11+
suspend: true
12+
concurrencyPolicy: Forbid
13+
successfulJobsHistoryLimit: 3
14+
failedJobsHistoryLimit: 3
15+
jobTemplate:
16+
spec:
17+
backoffLimit: 1
18+
ttlSecondsAfterFinished: 300
19+
template:
20+
metadata:
21+
annotations:
22+
metaboost.example.org/ops-job: db-drop-everything
23+
spec:
24+
restartPolicy: Never
25+
enableServiceLinks: false
26+
containers:
27+
- name: drop-everything
28+
image: postgres:18.3
29+
imagePullPolicy: Always
30+
command:
31+
- bash
32+
- -c
33+
- |
34+
set -euo pipefail
35+
export PGHOST="${DB_HOST:-metaboost-db}"
36+
export PGPORT="${DB_PORT:-5432}"
37+
export PGPASSWORD="${DB_APP_OWNER_PASSWORD}"
38+
psql -v ON_ERROR_STOP=1 -U "${DB_APP_OWNER_USER}" -d "${DB_APP_NAME}" \
39+
-c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'
40+
export PGPASSWORD="${DB_MANAGEMENT_OWNER_PASSWORD}"
41+
psql -v ON_ERROR_STOP=1 -U "${DB_MANAGEMENT_OWNER_USER}" -d "${DB_MANAGEMENT_NAME}" \
42+
-c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'
43+
envFrom:
44+
- secretRef:
45+
name: metaboost-db-secrets
46+
env:
47+
- name: DB_HOST
48+
value: metaboost-db
49+
- name: DB_PORT
50+
value: "5432"

infra/k8s/base/ops/db-rebootstrap-roles.cronjob.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Pattern: Suspended CronJob (Manual Trigger Only)
22
# Re-runs full StatefulSet bootstrap logic from `0001_create_app_db_users.sh` +
33
# `0002_setup_management_database.sh` over TCP (script: rebootstrap-full-bootstrap.sh).
4-
# Run after DROP SCHEMA public CASCADE or role/password drift so migrator roles and grants match init.
4+
# Run after metaboost-db-drop-everything or DROP SCHEMA public CASCADE so migrator roles and grants match init.
55
# Does not apply 0003 linear baselines — migrate jobs remain the forward path.
66
apiVersion: batch/v1
77
kind: CronJob

infra/k8s/base/ops/kustomization.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1
22
kind: Kustomization
33

44
resources:
5+
- db-drop-everything.cronjob.yaml
56
- db-migrate-app.cronjob.yaml
67
- db-migrate-management.cronjob.yaml
78
- db-verify-bootstrap-contract.cronjob.yaml

infra/k8s/base/ops/source/database/runner/rebootstrap-full-bootstrap.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,4 +171,12 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ${DB_MANAGEM
171171
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON SEQUENCES TO ${DB_MANAGEMENT_READ_USER};
172172
SQL
173173

174+
echo "rebootstrap-full-bootstrap: Phase D — uuid-ossp extension (same as 0003_apply_linear_baselines.sh)"
175+
176+
export PGPASSWORD="${DB_APP_OWNER_PASSWORD}"
177+
psql -v ON_ERROR_STOP=1 -U "${DB_APP_OWNER_USER}" -d "${DB_APP_NAME}" -c 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp";'
178+
179+
export PGPASSWORD="${DB_MANAGEMENT_OWNER_PASSWORD}"
180+
psql -v ON_ERROR_STOP=1 -U "${DB_MANAGEMENT_OWNER_USER}" -d "${DB_MANAGEMENT_NAME}" -c 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp";'
181+
174182
echo "rebootstrap-full-bootstrap: completed successfully."

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"db:status:linear:k8s:management": "bash scripts/database/print-linear-migrations-status-k8s.sh --database management",
5959
"db:validate:linear": "bash scripts/database/validate-linear-migrations.sh",
6060
"db:validate:linear:db": "bash scripts/database/validate-linear-migrations.sh --check-db",
61+
"db:ops:schema-reset:k8s": "bash scripts/database/run-ops-db-schema-reset-k8s.sh",
6162
"billing:process-due-renewals": "node scripts/api/process-due-renewals.mjs",
6263
"management:superuser:create": "node scripts/management-api/create-super-admin.mjs",
6364
"management:superuser:update": "node scripts/management-api/update-super-admin.mjs",
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env bash
2+
# Trigger the full Metaboost ops DB schema reset sequence in Kubernetes (one CronJob per step).
3+
#
4+
# Use when linear_migration_history checksums disagree with current migration files (e.g. after
5+
# editing already-applied SQL on a non-greenfield database). Destructive: drops public schema on
6+
# app and management databases.
7+
#
8+
# Prerequisites:
9+
# - kubectl context targets the intended cluster/namespace
10+
# - ops overlay synced (suspended CronJobs present)
11+
# - metaboost-db Running
12+
#
13+
# Usage:
14+
# export K8S_NAMESPACE=metaboost-alpha
15+
# bash scripts/database/run-ops-db-schema-reset-k8s.sh
16+
#
17+
# Each step creates a Job from the matching CronJob and waits for completion (default 15m timeout).
18+
19+
set -euo pipefail
20+
21+
NAMESPACE="${K8S_NAMESPACE:-}"
22+
WAIT_TIMEOUT="${OPS_DB_RESET_WAIT_TIMEOUT:-900s}"
23+
24+
if [[ -z "$NAMESPACE" ]]; then
25+
echo "ERROR: K8S_NAMESPACE must be set (e.g. export K8S_NAMESPACE=metaboost-alpha)." >&2
26+
exit 1
27+
fi
28+
29+
CRONJOBS=(
30+
metaboost-db-drop-everything
31+
metaboost-db-rebootstrap-roles
32+
metaboost-db-migrate-app
33+
metaboost-db-migrate-management
34+
metaboost-db-verify-bootstrap-contract
35+
metaboost-management-superuser-create
36+
)
37+
38+
run_cronjob_step() {
39+
local cronjob="$1"
40+
local job_name="${cronjob}-manual-$(date +%s)"
41+
42+
echo "==> Creating job ${job_name} from cronjob/${cronjob}"
43+
kubectl -n "$NAMESPACE" create job --from="cronjob/${cronjob}" "$job_name"
44+
45+
echo "==> Waiting for job/${job_name} (timeout ${WAIT_TIMEOUT})"
46+
if ! kubectl -n "$NAMESPACE" wait --for=condition=complete "job/${job_name}" --timeout="$WAIT_TIMEOUT"; then
47+
echo "ERROR: job/${job_name} did not complete successfully." >&2
48+
kubectl -n "$NAMESPACE" logs "job/${job_name}" --tail=80 || true
49+
exit 1
50+
fi
51+
52+
kubectl -n "$NAMESPACE" logs "job/${job_name}" --tail=40
53+
echo ""
54+
}
55+
56+
echo "Metaboost ops DB schema reset in namespace: ${NAMESPACE}"
57+
echo "Steps: ${CRONJOBS[*]}"
58+
echo ""
59+
60+
for cronjob in "${CRONJOBS[@]}"; do
61+
run_cronjob_step "$cronjob"
62+
done
63+
64+
echo "Schema reset sequence completed."
65+
echo "Next: rollout restart deployment/api deployment/management-api (and web tiers if needed)."

0 commit comments

Comments
 (0)