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
This commit is contained in:
Hanzo AI
2026-06-03 11:27:14 -07:00
parent 436b90b62f
commit 77c2300fd6
4 changed files with 550 additions and 0 deletions
+148
View File
@@ -0,0 +1,148 @@
#!/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"
+37
View File
@@ -0,0 +1,37 @@
# zoo-exchange.yaml -- Cloudflare zone config for zoo.exchange
# Source of truth for zoo.exchange DNS records
# Last synced: 2026-03-26
---
zone: zoo.exchange
records:
- type: A
name: "@"
content: "134.199.138.27"
proxied: false
- type: A
name: www
content: "134.199.138.27"
proxied: false
- type: A
name: gw
content: "134.199.138.27"
proxied: false
# Wildcard for gateway subdomain routing
- type: A
name: "*.gw"
content: "134.199.138.27"
proxied: false
- type: CNAME
name: docs
content: "2d14d338f3-hosting.gitbook.io"
proxied: false
- type: CNAME
name: api
content: "cname.vercel-dns.com"
proxied: false
+88
View File
@@ -0,0 +1,88 @@
# zoo-network.yaml — Cloudflare zone config for zoo.network
# Zone ID: d9c81c5b54f0f713105d483ddbd61656
# Source of truth for zoo.network DNS records
# Last synced: 2026-03-24
---
zone: zoo.network
zone_id: "d9c81c5b54f0f713105d483ddbd61656"
records:
- type: A
name: api-explore
content: "134.199.138.27"
proxied: false
- type: A
name: api-explorer
content: "67.205.158.227"
proxied: false
- type: A
name: api
content: "134.199.138.27"
proxied: false
- type: A
name: exchange
content: "134.199.138.27"
proxied: false
- type: A
name: explore
content: "134.199.138.27"
proxied: false
- type: A
name: iam
content: "134.199.138.27"
proxied: false
- type: A
name: mpc-api
content: "134.199.138.27"
proxied: false
- type: A
name: mpc
content: "134.199.138.27"
proxied: false
- type: A
name: status
content: "134.199.138.27"
proxied: false
- type: A
name: "@"
content: "185.199.111.153"
proxied: false
- type: A
name: "@"
content: "185.199.110.153"
proxied: false
- type: A
name: "@"
content: "185.199.109.153"
proxied: false
- type: A
name: "@"
content: "185.199.108.153"
proxied: false
- type: CNAME
name: id
content: "hanzo-id.pages.dev"
proxied: true
- type: CNAME
name: www
content: "zoo-labs.github.io"
proxied: false
- type: TXT
name: "_acme-challenge.mpc"
content: '3xcQ9lIkiW2iU0lB1f2ZSjLTo7G9StXvLC6jKR59B7M'
ttl: 120
+277
View File
@@ -0,0 +1,277 @@
# zoo-ngo.yaml — Cloudflare zone config for zoo.ngo
# Zone ID: 9c6372f69d53e64bc2ba21b96263fceb
# Source of truth for zoo.ngo DNS records
# Last synced: 2026-03-12
---
zone: zoo.ngo
zone_id: "9c6372f69d53e64bc2ba21b96263fceb"
records:
- type: A
name: api-explore
content: "134.199.138.27"
proxied: false
- type: A
name: api-test
content: "34.22.245.55"
proxied: false
- type: A
name: api.explore-test
content: "34.22.245.55"
proxied: false
- type: A
name: api.explore
content: "34.22.248.74"
proxied: false
- type: A
name: api.web3
content: "134.199.136.205"
proxied: false
ttl: 300
- type: A
name: api
content: "34.22.248.74"
proxied: false
- type: A
name: bridge
content: "134.199.138.27"
proxied: false
- type: A
name: explore-test
content: "34.22.245.55"
proxied: false
- type: A
name: explore
content: "134.199.138.27"
proxied: false
- type: A
name: explorer
content: "134.199.138.27"
proxied: false
- type: A
name: kms
content: "134.199.138.27"
proxied: false
- type: A
name: sig.explore-test
content: "34.22.245.55"
proxied: false
- type: A
name: sig.explore
content: "34.22.248.74"
proxied: false
- type: A
name: stats.explore-test
content: "34.22.245.55"
proxied: false
- type: A
name: stats.explore
content: "34.22.248.74"
proxied: false
- type: A
name: viz.explore-test
content: "34.22.245.55"
proxied: false
- type: A
name: viz.explore
content: "34.22.248.74"
proxied: false
- type: A
name: web3
content: "134.199.136.205"
proxied: false
ttl: 300
- type: A
name: ws.web3
content: "134.199.136.205"
proxied: false
ttl: 300
- type: A
name: "@"
content: "185.199.111.153"
proxied: false
- type: A
name: "@"
content: "185.199.110.153"
proxied: false
- type: A
name: "@"
content: "185.199.109.153"
proxied: false
- type: A
name: "@"
content: "185.199.108.153"
proxied: false
- type: AAAA
name: cdn
content: "100::"
proxied: true
- type: AAAA
name: "@"
content: "2606:50c0:8003::153"
proxied: false
- type: AAAA
name: "@"
content: "2606:50c0:8001::153"
proxied: false
- type: AAAA
name: "@"
content: "2606:50c0:8000::153"
proxied: false
- type: AAAA
name: "@"
content: "2606:50c0:8002::153"
proxied: false
- type: CNAME
name: ai
content: "zooai-chat.pages.dev"
proxied: true
- type: CNAME
name: blog
content: "zoo-blog.pages.dev"
proxied: true
- type: CNAME
name: chat
content: "zooai-chat.pages.dev"
proxied: true
- type: CNAME
name: donate
content: "hosted-checkout.stripecdn.com"
proxied: false
- type: CNAME
name: download
content: "public.r2.dev"
proxied: true
- type: CNAME
name: foundation
content: "zoo-apps.github.io"
proxied: false
- type: CNAME
name: gym
content: "zooai.github.io"
proxied: false
- type: CNAME
name: papers
content: "zooai.github.io"
proxied: false
- type: CNAME
name: ui
content: "zoo-labs.github.io"
proxied: false
- type: CNAME
name: vote
content: "zoo-apps.github.io"
proxied: false
- type: CNAME
name: www
content: "zoo-apps.github.io"
proxied: false
- type: CNAME
name: zips
content: "zooai.github.io"
proxied: true
- type: MX
name: "@"
content: "alt3.aspmx.l.google.com"
ttl: 3600
- type: MX
name: "@"
content: "aspmx.l.google.com"
ttl: 3600
- type: MX
name: "@"
content: "alt1.aspmx.l.google.com"
ttl: 3600
- type: MX
name: "@"
content: "alt2.aspmx.l.google.com"
ttl: 3600
- type: MX
name: "@"
content: "alt4.aspmx.l.google.com"
ttl: 3600
- type: TXT
name: "_acme-challenge.api-explore"
content: 'aTIhdbcShrcYpSFE2r_4C6J_11pE89XdKDOpknbv_Os'
ttl: 120
- type: TXT
name: "_acme-challenge.donate"
content: 'mGXhVquq8VRifQvM3q_x3zYD1yk00RvpmwU_WSroCzQ'
- type: TXT
name: "_acme-challenge.explore"
content: 'p3a8omZVV0qfpdq9YEAJ4uY6nexvzZBJcL5OcCqOP2A'
ttl: 120
- type: TXT
name: "_vercel"
content: 'vc-domain-verify=ai.zoo.ngo,4b57259b626ea7fedef2'
- type: TXT
name: "@"
content: 'slack-domain-verification=DRNH0gwucEBt7vpjJZWfkMt7rIWmGJRx6aCoz4UF'
- type: TXT
name: "@"
content: 'b3c899da-4a5e-4e4e-ada1-450fdefbb131=fe1b76ad366126398732fc4cd8489b520d01174ceec3fe2a6defa8cdfc84fb26'
- type: TXT
name: "@"
content: 'google-site-verification=PI_SalILhgyYqTKFOoSzV9FkIeaKSsVSOeBwEXT9reI'
ttl: 3600
- type: TXT
name: "@"
content: 'v=spf1 include:_spf.google.com -all'
ttl: 3600
- type: TXT
name: "@"
content: 'google-site-verification=YZ6z_uNp6bkHHKxDXnBKSEr6tVMR4mai2q35AJ_Gu-A'
ttl: 3600