Files
CloudOps/ansible/sanitize_k8s_manifest.py
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

61 lines
1.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""sanitize_k8s_manifest.py — strip source-cluster-specific fields from a
captured `kubectl get ... -o yaml` dump so it can be applied fresh onto a
different (DR/standby) cluster.
backup-k8s-apps.sh's manifests.yaml/pvc.yaml/secret.yaml are raw `kubectl
get -o yaml` output: server-assigned fields like resourceVersion/uid are
always present, and PVCs additionally carry spec.volumeName + a Bound
status pointing at a specific PersistentVolume that only exists on the
ORIGINAL cluster. Applying any of that verbatim onto a fresh cluster either
fails outright (a stale resourceVersion trips optimistic-concurrency
checks — the exact bug fixed in restore-k8s-apps.sh's Secret handling) or
silently tries to bind a PVC to a PV that was never created there.
Usage: sanitize_k8s_manifest.py <in.yaml> <out.yaml>
"""
import sys
import yaml
DROP_METADATA_KEYS = (
'resourceVersion', 'uid', 'creationTimestamp', 'selfLink',
'generation', 'finalizers', 'managedFields', 'annotations',
)
def sanitize_item(item):
meta = item.get('metadata') or {}
for key in DROP_METADATA_KEYS:
meta.pop(key, None)
item.pop('status', None)
if item.get('kind') == 'PersistentVolumeClaim':
spec = item.get('spec') or {}
spec.pop('volumeName', None)
return item
def main():
if len(sys.argv) != 3:
print(f"usage: {sys.argv[0]} <in.yaml> <out.yaml>", file=sys.stderr)
sys.exit(1)
with open(sys.argv[1]) as f:
doc = yaml.safe_load(f)
if doc is None:
items = []
elif doc.get('kind') == 'List':
items = [sanitize_item(i) for i in doc.get('items', [])]
else:
items = [sanitize_item(doc)]
with open(sys.argv[2], 'w') as f:
yaml.safe_dump_all(items, f, default_flow_style=False)
if __name__ == '__main__':
main()