Files
Hanzo AI 77c2300fd6 k8s/cf-zones: adopt zoo.network, zoo.exchange, zoo.ngo DNS zone YAMLs
Coordination with the Lux universe DX cleanup — Cloudflare zone YAMLs
are pure brand assets and belong with their owning universe. Moved
2026-06-03 from lux/universe/k8s/dns/{zoo-exchange,zoo-network,zoo-ngo}.yaml.

k8s/cf-zones/sync.sh is the per-brand drift-detect-and-PATCH script
(same logic as the legacy lux/universe scripts/dns-sync.sh, scoped to
this directory). Dry-run by default; set CF_API_TOKEN and pass --apply
to push changes.

Single-zone usage:
  ./k8s/cf-zones/sync.sh --zone zoo-ngo
2026-06-03 11:27:14 -07:00

149 lines
4.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# infra/cf-zones/sync.sh — Zoo brand Cloudflare zone sync.
#
# Mirrors the per-zone YAML files in this directory into the matching
# Cloudflare zone via the CF API. Skips proxied records (managed via
# the Cloudflare UI). Only updates records that have drifted from the
# YAML source-of-truth.
#
# Usage:
# ./sync.sh # Dry run (show what would change)
# ./sync.sh --apply # Apply changes to Cloudflare
# ./sync.sh --zone hanzo-id # Sync a single zone (filename stem)
#
# Prerequisites:
# CF_API_TOKEN env var (Cloudflare API token with DNS edit permission)
# python3 with stdlib only (no PyYAML — uses inline parser)
#
# Moved 2026-06-03 from lux/universe/scripts/dns-sync.sh as part of
# brand-boundary cleanup. Lux universe keeps its own copy that only
# reads lux-*.yaml; per-brand zones are owned by per-brand universes.
set -euo pipefail
DNS_DIR="$(cd "$(dirname "$0")" && pwd)"
DRY_RUN=true
SINGLE_ZONE=""
for arg in "$@"; do
case $arg in
--apply) DRY_RUN=false ;;
--zone=*) SINGLE_ZONE="${arg#*=}" ;;
--zone) shift; SINGLE_ZONE="${1:-}" ;;
esac
done
CF_TOKEN="${CF_API_TOKEN:-${CLOUDFLARE_DNS_TOKEN:-}}"
if [ -z "$CF_TOKEN" ] && [ "$DRY_RUN" = false ]; then
echo "ERROR: CF_API_TOKEN or CLOUDFLARE_DNS_TOKEN required for --apply"
exit 1
fi
echo "=== Zoo CF Zone Sync $([ "$DRY_RUN" = true ] && echo '(DRY RUN)' || echo '(APPLYING)') ==="
echo "Zone dir: $DNS_DIR"
echo ""
for zone_file in "$DNS_DIR"/*.yaml; do
zone_name=$(basename "$zone_file" .yaml)
if [ -n "$SINGLE_ZONE" ] && [ "$zone_name" != "$SINGLE_ZONE" ]; then
continue
fi
python3 - "$zone_file" "$CF_TOKEN" "$DRY_RUN" <<'PY'
import json, os, subprocess, sys, urllib.request
zone_file = sys.argv[1]
token = sys.argv[2]
dry_run = sys.argv[3] == "true"
try:
import yaml
except ImportError:
sys.stderr.write("pyyaml not installed; install with: pip install pyyaml\n")
sys.exit(2)
with open(zone_file) as f:
d = yaml.safe_load(f) or {}
zone = d.get("zone", "")
zone_id = d.get("zone_id", "")
records = d.get("records", [])
if not zone or not zone_id:
print(f"SKIP: {zone_file} (no zone/zone_id)")
sys.exit(0)
print(f"--- {zone} (ID: {zone_id[:8]}...) ---")
checked = drifted = updated = 0
for r in records:
rtype = r.get("type", "")
name = r.get("name", "")
content = r.get("content", "")
proxied = r.get("proxied", False)
if rtype not in ("A", "CNAME"):
continue
if name == "@":
continue
if proxied:
continue
fqdn = f"{name}.{zone}" if name != zone else zone
try:
result = subprocess.run(
["dig", "+short", fqdn, "@1.1.1.1"],
capture_output=True, text=True, timeout=5,
)
live = result.stdout.strip().split("\n")[0] if result.stdout.strip() else ""
except Exception:
live = ""
checked += 1
if live == content:
continue
drifted += 1
print(f" DRIFT: {fqdn} live={live or '(none)'} yaml={content}")
if dry_run or not token:
continue
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records?type={rtype}&name={fqdn}"
try:
req = urllib.request.Request(url, headers=headers)
cf_data = json.loads(urllib.request.urlopen(req, timeout=10).read())
cf_records = cf_data.get("result", [])
if cf_records:
rec_id = cf_records[0]["id"]
patch_url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{rec_id}"
patch_data = json.dumps({"content": content, "proxied": proxied}).encode()
patch_req = urllib.request.Request(patch_url, data=patch_data, headers=headers, method="PATCH")
patch_result = json.loads(urllib.request.urlopen(patch_req, timeout=10).read())
if patch_result.get("success"):
print(f" UPDATED: {fqdn} -> {content}")
updated += 1
else:
print(f" FAILED: {patch_result.get('errors')}")
else:
create_url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records"
create_data = json.dumps({"type": rtype, "name": fqdn, "content": content, "proxied": proxied}).encode()
create_req = urllib.request.Request(create_url, data=create_data, headers=headers, method="POST")
create_result = json.loads(urllib.request.urlopen(create_req, timeout=10).read())
if create_result.get("success"):
print(f" CREATED: {fqdn} -> {content}")
updated += 1
else:
print(f" FAILED: {create_result.get('errors')}")
except Exception as e:
print(f" ERROR: {e}")
print(f" Checked: {checked}, Drifted: {drifted}, Updated: {updated}")
PY
echo ""
done
echo "=== Done ==="
[ "$DRY_RUN" = true ] && echo "Run with --apply to push changes to Cloudflare"