#!/usr/bin/env python3 """Return adopted routes to Helm only after root and child writers are quiescent.""" import argparse import json from pathlib import Path import subprocess import sys ROOT = Path(__file__).resolve().parents[1] sys.path[:0] = [str(ROOT / 'dx/common/scripts'), str(ROOT / 'devops/platform/scripts')] from devbox.common import Target # noqa: E402 from platform_gitops import model, reconcile # noqa: E402 from platform_gitops.kubernetes import Cluster # noqa: E402 def rollback(target): target.assert_cluster() cluster = Cluster(target.kubeconfig) owner = cluster.owner() if not owner or owner['data']['routes'] not in {'adopting', 'argo', 'restoring'}: raise ValueError('No recorded Argo route ownership to recover') values = target.directory / 'routes-before-adoption.json' if not values.is_file() or values.is_symlink(): raise ValueError('Restore the private route values backup before changing ownership') reconcile.suspend(cluster) # The window this recovery opens is its own. A record can still carry the predecessor an # interrupted retirement wrote, and that predecessor names Argo ownership this recovery is # replacing with the Helm chart: resuming it would fence the wrong writer. Clearing it leaves # a window with no recorded predecessor, which a retirement refuses to resume and this script # - identity-checked and safe to repeat - is the only way to finish. cluster.write_owner({'routes': reconcile.WINDOW, reconcile.PREVIOUS: None}) # Clear only Argo tracking after suspension. Preserve resource identities, # policy specs, trust Secrets, Helm history, data and namespaces. route_inputs = json.loads(owner['data']['inputs']) documents = reconcile.routes(route_inputs) before = reconcile.identities(cluster, documents) for desired in documents: meta = desired['metadata'] cluster.run('-n', meta['namespace'], 'annotate', desired['kind'] + '.' + desired['apiVersion'].split('/')[0], meta['name'], 'argocd.argoproj.io/tracking-id-') subprocess.run(['helm', '--kubeconfig', str(target.kubeconfig), 'upgrade', 'pharos-platform-routes', str(model.ROUTES_CHART), '-n', 'istio-system', '-f', str(values), '--reset-values', '--atomic', '--wait', '--timeout', '300s'], check=True) after = reconcile.identities(cluster, documents) if before != after: raise ValueError('Route identity changed during recovery; retain restoring fence') reconcile.close_window(cluster, 'ansible') print('Routes returned to Helm with unchanged UIDs; root and children remain suspended. ' 'Use make deploy from the owner checkout to restore Argo adoption.') def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--target', required=True) parser.add_argument('--component', choices=['routes', 'argocd'], default='routes') args = parser.parse_args() target = Target(args.target) with target.lock(timeout=60): if args.component == 'routes': rollback(target) else: target.assert_cluster() cluster = Cluster(target.kubeconfig) if not cluster.owner(): raise ValueError('Argo bootstrap recovery requires a recorded owner') reconcile.suspend(cluster) cluster.write_owner({'argocd': 'bootstrap'}) print('Root and children are quiescent; Argo bootstrap writer restored explicitly. ' 'Route ownership remains fenced. Repeat make deploy for recovery.') if __name__ == '__main__': main()