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.
61 lines
1.8 KiB
Python
Executable File
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()
|