Commit Graph

8 Commits

Author SHA1 Message Date
root
054a9e1fcb Add Ansible-driven DR bootstrap (#9) + PVC capture in backups
restore-k8s-apps.sh's own header explicitly scoped out fresh-cluster
provisioning ("assumes the target cluster/namespaces/PVCs already
exist"). This adds exactly that missing piece as an Ansible playbook
rather than another bash script — a better fit for "provision a target
into a working state" than for the actual data-restore logic, which stays
in restore-k8s-apps.sh unchanged and is just invoked as the final step.

dr-bootstrap.yml, given an app name + a backup archive:
  1. Checks the target's environment (disk/mem/cpu) against k3s minimums
     before touching anything
  2. Installs k3s only if not already present (checked, not assumed)
  3. Creates the namespace, PVC(s), Secret, and Deployments/Services/
     Ingress from the backup's captured manifests — sanitized first
     (sanitize_k8s_manifest.py strips resourceVersion/uid/status/
     volumeName, the same class of bug as the earlier Secret-apply fix,
     generalized) since a raw `kubectl get -o yaml` dump can't be
     reapplied to a different cluster as-is
  4. Hands off to the existing, already-validated restore-k8s-apps.sh for
     the actual data population

backup-k8s-apps.sh gets a small but necessary addition: PVC specs were
never captured at all before (only Deployment/Service/ConfigMap/Ingress),
so there was nothing for a fresh-cluster bootstrap to provision the PVC
from. Captured with the same label selector as the existing manifests
capture, so it stays in lockstep automatically.

Verified for real, not just syntax-checked: ran the full playbook against
the standby VM (freshly installed k3s, empty namespace) using a live n8n
backup. Namespace/PVCs/Secret/Deployments provisioned from nothing, data
restored, pod healthy, and the workflow count in the restored Postgres DB
(11) matched the real source server exactly.
2026-08-21 14:30:58 +02:00
root
459c2974f0 Add VM-side backup retention (was completely unbounded)
Checking disk usage on the VM (95% full, blocking the planned k3s
install) found /backups/cloudproject had 85 legacy Docker-era
myapps-backup-* archives totaling 131GB, dating back to May — the whole
tier had never had any retention at all, only local (TIER 1) and R2
(TIER 3) did. Those 85 are from before the k8s migration entirely (the
Docker volumes they captured don't exist anymore), so deleted all of
them directly on the VM rather than just trimming a few.

Added retention to the VM upload step mirroring the existing local
policy exactly: same MAX_BACKUPS, own myapps-k8s-backup-* glob only —
never touches the other unrelated backup directories that also live on
this VM (imcbackups/mpack/garage, other servers' backups, already
separately reviewed and left alone except a manual one-off trim of
garage's uncapped 31-day history down to 10).

Disk usage: 183G/193G (95%) -> 50G/193G (26%) after cleanup.
2026-08-21 14:07:22 +02:00
root
995e07c894 Fix two restore-k8s-apps.sh bugs found verifying Nextcloud restore
1. Secret apply conflict: secret.yaml is a raw `kubectl get -o yaml` dump
   that includes the live resourceVersion/uid/creationTimestamp at capture
   time. Applying a stale resourceVersion trips optimistic-concurrency
   control ("the object has been modified") on effectively every restore,
   since normal cluster activity keeps bumping it. Strip those three
   volatile fields before `kubectl apply`.

2. Wrong re-seed source for Nextcloud: the re-seed step added for the PVC
   exclusion fix assumed both Mautic and Nextcloud bake their excluded dirs
   in at their own PVC mount path (/var/www/html), copying from
   pvc_path/$dir. True for Mautic, wrong for Nextcloud — its image keeps
   the shipped source tree at /usr/src/nextcloud, completely separate from
   /var/www/html (empty in the raw image at that path). Caught in testing
   ("cp: cannot stat '/var/www/html/apps'") because the reseed_failed gate
   from the previous fix did its job and left the app at 0 replicas instead
   of booting broken — but still required a live manual fix to bring
   Nextcloud back after the failed test. New RECONSTRUCT_SRC table makes
   the copy source explicit and independently verified per app instead of
   assumed by analogy: confirmed /usr/src/nextcloud/{apps,core,dist,
   3rdparty,lib} exist in a bare nextcloud:32 pod with no volume mounted
   before relying on it.

Verified end-to-end after both fixes: fresh Nextcloud backup -> restore ->
data/ intact (184M, 89 files, unchanged) -> occ status installed/healthy ->
200 on /status.php, via the actual script run (not just the manual
recovery), confirming the automated path works, not just my live fix.
2026-08-21 12:37:16 +02:00
root
fca77d92b5 Exclude reproducible framework code from Mautic/Nextcloud PVC backups
Mautic (node_modules/, vendor/) and Nextcloud (apps/, core/, dist/,
3rdparty/, lib/) share one PVC mount with their real user data, and were
backing up ~700MB-1GB of app/vendor code that's identical to what ships in
the image, alongside the actual user data. Excluded both via tar --exclude
in the PVC capture step: Mautic backup drops from ~185MB to ~90MB
compressed, Nextcloud from ~379MB to ~107MB.

Verified the "the image lays these down fresh on start" assumption before
relying on it — it does NOT hold for either app as a naive exclude:
  - Mautic's entrypoint has no logic at all to reconstruct node_modules/
    vendor if missing (confirmed empty by reading /entrypoint.sh).
  - Nextcloud's entrypoint only rsyncs from /usr/src/nextcloud when
    image_version > installed_version (read from version.php). Since
    version.php itself isn't excluded, a restored version.php already
    matches the running image's version, so that path never fires.

So restore-k8s-apps.sh now explicitly re-seeds these dirs from the app's
own live image (both images bake them in at /var/www/html independently of
any volume mount) via a loader pod with the PVC mounted at a different
path, after the normal PVC-data restore and before scaling the app back
up. If that re-seed fails, the app is deliberately left at 0 replicas
instead of coming back up broken (missing vendor/autoload.php is a hard
crash, not a degraded state) — found this the hard way in testing when a
90s wait timed out mid image-pull and scale-up proceeded anyway with
vendor/ missing; fixed by gating scale-up on the re-seed outcome and
raising the timeout to 240s with imagePullPolicy: IfNotPresent (the image
is already on-node, pulled off the live Deployment spec).

Verified end-to-end: real backup + restore of Mautic on the live cluster,
confirming size drop, vendor/autoload.php present post-restore, app
serving 200s, and all 433,681 leads intact via the DB restore.

Also found and left unfixed (pre-existing, unrelated): secret.yaml apply
during restore can hit a resourceVersion conflict from kubectl apply
against a captured manifest — the mautic-secrets apply failed on this
restore test with a benign "object has been modified" error since the
secret already existed with correct values; app was unaffected. Separate
bug from this change, flagging for later.
2026-08-21 11:07:02 +02:00
root
d498c81d5b Add per-backup status dot (green/yellow/red/unknown) to the UI
backup-k8s-apps.sh now tracks step-level failures (manifests, secret,
db_dump, pvc_data) into an ERRORS array during the per-app loop, and
writes a .meta.json sidecar alongside each archive once all 3 storage
tiers are known - apps included, final size, the error list, and
per-tier ok/failed/skipped status. Sidecar is mirrored to VM/R2 the
same best-effort way the .sha256 sidecar already is, and cleaned up
by both local and R2 retention pruning plus manual delete.

modules/backups.py reads these sidecars (never decompresses the
archive) to compute a status per backup at list-render time:
  - red: any recorded error, or any tier status starting with "failed"
  - yellow: no errors, but size deviates >40% from the rolling average
    of the last 5 backups sharing the same app-combination (apps_key)
  - green: no errors, size within range (or first backup of its
    app-combination - nothing to compare against yet)
  - unknown: no sidecar at all (legacy myapps-backup-* archives, or
    any k8s backup made before this shipped) - no backfill attempted,
    old runs never recorded step-level failures to reconstruct from

/backups route now passes get_local_backups_with_status()/
get_vm_backups_with_status() instead of the plain filename lists
(get_local_backups()/get_vm_backups() themselves are untouched -
/restore and /api/backups still use the plain versions, they don't
need the dot). Template renders a colored dot next to each entry with
a tooltip showing apps/size.

Verified: real n8n backup produces a correct sidecar synced to all 3
tiers; JSON-writer argv parsing and the red/yellow/green/unknown
decision logic each checked against synthetic cases; full Jinja render
checked against real local + VM data pulled from the live pod.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017avLHFqkiti3g62Anq9sVA
2026-08-21 02:46:50 +02:00
root
49b68cf820 Lower backup/restore RAM safety threshold 1536MB -> 1024MB
Investigated a resource-safety abort during Odoo restore testing:
available RAM was hovering ~1.3-1.5GB even under normal conditions on
this host (k3s + Jenkins + Docker monitoring/wazuh stack + dev tooling
baseline), not a leak from any single process. 1536MB was tripping on
ordinary single-app backups; 1024MB still leaves real headroom above
what a pg_dump/mysqldump/tar step actually needs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017avLHFqkiti3g62Anq9sVA
2026-08-21 01:43:37 +02:00
root
f3f08c3ef6 Give management-platform a scoped ServiceAccount for k8s backup/restore
The pod running app.py had no kubectl binary and only a read-only SA
(management-platform-viewer-sa) — backup-k8s-apps.sh/restore-k8s-apps.sh
could never actually run from inside it. Adds:

- management-platform-backup-role (ClusterRole, bound via RoleBinding only
  in n8n/odoo/mautic/nextcloud/erpnext — not cluster-wide): get/list/watch
  on pods/deployments/configmaps/ingresses/PVCs, pods/exec create, pods
  create+delete (needed for restore's populate-before-scale-up loader pod),
  secrets get/list/update/patch, deployments/scale update/patch only (no
  write on full Deployment/Service/ConfigMap/Ingress specs).
- management-platform-sa, replacing management-platform-viewer-sa as the
  pod's identity — inherits the old viewer-role's read-only bindings too
  (retargeted in management-platform-viewer-rbac.yaml) so the existing
  cluster-view page keeps working under one SA.
- k3s binary hostPath-mounted read-only into the pod (same pattern as the
  Jenkins agent's docker-cli container), so `k3s kubectl` is available
  where no separate kubectl binary exists.

Both scripts updated to fall back to k3s kubectl when no kubectl is on
PATH, and to use /proc/meminfo instead of `free` for the resource-safety
check (not present in the pod's minimal image). Also fixes a real issue
found while live-testing this from inside the pod: the pod's pre-existing
/root hostPath mount also exposes the host's own admin ~/.kube/config,
which k3s kubectl was silently preferring over the scoped SA token —
forcing --kubeconfig=/dev/null in both scripts closes that.

Verified end-to-end from inside the actual management-platform pod:
manifests/secret/DB-dump/PVC-data backup for n8n succeeds under the new
SA's scoped permissions, and a kube-system access attempt is correctly
rejected (Forbidden) once the kubeconfig leak is closed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 11:30:03 +02:00
root
8255527254 Add k8s-native backup/restore scripts for the 5 live apps
backup-myapps.sh/restore-myapps.sh only ever knew about Docker volumes,
which no longer exist for n8n/odoo/mautic/nextcloud/frappe now that
they're all running in k3s — nightly backups have been silently hollow
for these apps since the migration. These new scripts capture/restore
each app's k8s manifests, Secret, DB (pg_dump/mysqldump via kubectl exec),
and app-data PVC contents (kubectl exec tar, never raw filesystem access),
reusing the existing local+VM+R2 3-2-1 storage/retention logic unchanged.

Tested standalone: full backup of all 5 apps produces verified non-empty
manifests/secrets/DB dumps/PVC data; restore-in-place tested end-to-end
against n8n (DB row counts and PVC data matched exactly pre/post, app
verified healthy over HTTPS).

Not wired into management-platform yet (app.py stays untouched) — restore
script also doesn't yet handle a fresh/DR cluster where the PVCs and their
manifests don't already exist (same-cluster restore only for now).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 11:03:14 +02:00