#!/usr/bin/env nu # Set up quian (NBG1) as ops launch node for libre-wuji (FSN1). # # Topology: # M4 Mac → SSH → quian (46.225.25.227, pub; 10.0.9.2, wuwei NBG1 subnet) # quian → SSH → libre-wuji-cp-0 (10.0.8.20, wuwei FSN1 subnet) # # Both are on the wuwei private network (10.0.8.0/22) but in different zones. # Hetzner multi-location VPC routes 10.0.9.x ↔ 10.0.8.x automatically. # M4 has NO direct access to 10.0.8.x — all control-plane ops go via quian. # # SSH TRUST SETUP (manual prerequisite): # Run with --print-pubkey, add the printed key to libre-wuji-cp-0 authorized_keys, # then re-run without that flag. def main [ --quian-host: string = "46.225.25.227" --wuji-cp-priv: string = "10.0.8.20" --nats-node-port: int = 34222 --print-pubkey # print quian pubkey for manual trust setup, then exit --skip-nats-patch # skip NATS NodePort patch (e.g. already done) --dry-run ]: nothing -> nothing { let quian = $quian_host let wuji_cp = $wuji_cp_priv if $dry_run { print "[dry-run] would: install kubectl on quian, copy admin.conf quian→wuji-cp, patch NATS NodePort" return } # ── Get/generate quian's SSH pubkey ──────────────────────────────────────── let gen_r = do { ^ssh $"root@($quian)" 'if [ ! -f ~/.ssh/id_ed25519 ]; then ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519 -q; fi' } | complete if $gen_r.exit_code != 0 { error make { msg: $"failed to generate ssh key on quian: ($gen_r.stderr | str trim)" } } let pubkey_r = do { ^ssh $"root@($quian)" 'cat ~/.ssh/id_ed25519.pub' } | complete if $pubkey_r.exit_code != 0 { error make { msg: $"failed to read pubkey from quian: ($pubkey_r.stderr | str trim)" } } let quian_pubkey = $pubkey_r.stdout | str trim if $print_pubkey { print "── quian SSH public key ──────────────────────────────────────────" print $quian_pubkey print "" print "Add this to libre-wuji-cp-0 /root/.ssh/authorized_keys:" print $" echo '($quian_pubkey)' >> /root/.ssh/authorized_keys" return } # ── Verify cross-zone routing (NBG1 → FSN1 via wuwei) ──────────────────── print $"── verify cross-zone routing quian → ($wuji_cp) ──" let ping_r = do { ^ssh $"root@($quian)" $"ping -c 3 -W 2 ($wuji_cp)" } | complete if $ping_r.exit_code != 0 { print $"FAIL: quian cannot reach ($wuji_cp) via private network" print "Check: hcloud network describe wuwei — locations must include both nbg1 and fsn1" error make { msg: "cross-zone routing not available — cannot proceed" } } print $"OK: quian (NBG1 10.0.9.2) → libre-wuji-cp-0 (FSN1 ($wuji_cp)) routed via wuwei" # ── Install kubectl on quian ────────────────────────────────────────────── print "── install kubectl (linux/arm64) on quian ──" let kubectl_r = do { ^ssh $"root@($quian)" ' if command -v kubectl >/dev/null 2>&1; then kubectl version --client 2>/dev/null || true exit 0 fi KVER=$(curl -Ls https://dl.k8s.io/release/stable.txt) curl -Lo /usr/local/bin/kubectl "https://dl.k8s.io/release/${KVER}/bin/linux/arm64/kubectl" chmod +x /usr/local/bin/kubectl kubectl version --client ' } | complete if $kubectl_r.exit_code != 0 { error make { msg: $"kubectl install failed on quian: ($kubectl_r.stderr | str trim)" } } print $kubectl_r.stdout # ── Copy admin.conf from libre-wuji-cp-0 via quian SSH ─────────────────── print "── copy admin.conf from libre-wuji-cp-0 (via quian SSH trust) ──" let mkdir_r = do { ^ssh $"root@($quian)" 'mkdir -p ~/.kube' } | complete if $mkdir_r.exit_code != 0 { error make { msg: "failed to create ~/.kube on quian" } } # Test SSH trust before attempting scp — give a clear error if missing let trust_r = do { ^ssh $"root@($quian)" $"ssh -o BatchMode=yes -o ConnectTimeout=5 root@($wuji_cp) echo ok" } | complete if $trust_r.exit_code != 0 { print "SSH trust from quian to libre-wuji-cp-0 is NOT established." print "" print $"Run: nu setup_quian.nu --quian-host ($quian) --print-pubkey" print $"Then on libre-wuji-cp-0: echo '' >> /root/.ssh/authorized_keys" print "Then re-run this script." error make { msg: "SSH trust missing — quian cannot reach libre-wuji-cp-0" } } let scp_r = do { ^ssh $"root@($quian)" $"scp -o StrictHostKeyChecking=no root@($wuji_cp):/etc/kubernetes/admin.conf ~/.kube/config && chmod 600 ~/.kube/config" } | complete if $scp_r.exit_code != 0 { error make { msg: $"admin.conf copy failed: ($scp_r.stderr | str trim)" } } print "admin.conf installed at quian ~/.kube/config" # ── Verify kubectl works from quian ─────────────────────────────────────── print "── verify kubectl from quian ──" let nodes_r = do { ^ssh $"root@($quian)" 'kubectl get nodes -o wide' } | complete if $nodes_r.exit_code != 0 { error make { msg: $"kubectl failed from quian: ($nodes_r.stderr | str trim)" } } print $nodes_r.stdout # ── Patch NATS service to NodePort ──────────────────────────────────────── if not $skip_nats_patch { print $"── patch NATS svc to NodePort ($nats_node_port) ──" # Build patch as Nushell record → JSON, then pass to kubectl let patch_json = { spec: { type: "NodePort" ports: [ { name: "nats", port: 4222, targetPort: 4222, nodePort: $nats_node_port } { name: "monitor", port: 8222, targetPort: 8222 } ] } } | to json --raw let patch_r = do { ^ssh $"root@($quian)" $"kubectl patch svc nats -n core-service --type=merge -p '($patch_json)'" } | complete if $patch_r.exit_code != 0 { print $"WARNING: NATS patch failed \(may already be NodePort\): ($patch_r.stderr | str trim)" } else { print $"NATS NodePort active: ($wuji_cp):($nats_node_port)" } } print "" print "── quian is ready as ops launch node ────────────────────────────" print $" kubectl: operational from quian \(libre-wuji cluster\)" print $" NATS NodePort: ($wuji_cp):($nats_node_port)" print "" print "Next: deploy zot into libre-wuji K8s" print " scp extensions/components/zot/cluster/install-zot.sh root@46.225.25.227:/tmp/" print $" ssh root@($quian)" print " ZOT_NAMESPACE=registry ZOT_S3_ACCESS_KEY=... ZOT_S3_SECRET_KEY=... bash /tmp/install-zot.sh install" }