Provider-abstracted floating/reserved-IP mobility and ingress failover (R594-F5, R859-F2).
[FloatingIpProvider] is the domain-level trait each vendor adapter
implements; [reconcile_assignment] is the shared idempotent + zone-checked
core all of them run through, so the "no-op when already assigned" /
"reject a cross-zone move" behaviour is written and tested exactly once
instead of once per vendor. [plan_ingress_owner_effect] is the pure
decision layer above them: given a raft ingress_owner observation, what
should public ingress do?
Why this is its own crate
It was cloud::provider::floating_ip until 2026-09-08. R859-F2 landed
[plan_ingress_owner_effect] with no production caller, because the caller
belongs in yubaba's scheduler tick and a runtime yubaba -> cloud
dependency was not a courier's call to make: cloud/Cargo.toml's
yah-local-driver comment records R374-F3 carving that crate out of
cloud specifically to avoid such an edge, and taking it here would pull
velveteen, velveteen-exec, yah-hetzner, yah-mesofact-bundle and yah-almanac
into the release daemon shipped to every fleet node. The operator's answer
(2026-09-08) was to repeat R374-F3's move rather than reverse it — hence
this crate, which both cloud and yubaba depend on and neither owns.
So the dependency budget here is load-bearing, not tidiness: anyhow and
async-trait, and nothing else, forever. A reqwest or a serde in this
manifest ships to every node in the fleet.
What is above this crate, and where
Two consumers, and this crate depends on neither:
yah-floating-ip-adapters— the Hetzner/OVH/Vultr HTTP clients that implement [FloatingIpProvider]. They were inclouduntil R859-F3 moved them one layer down soyubaba's ingress effector could actually issue a reassign rather than only decide on one. That crate carries thereqwestthis one refuses to.yah-cloud— thefloating_ip.*envoy verb layer (cloud::provider::floating_ip_envoy) and the credentialed constructorcloud::provider::floating_ip::floating_ip_provider_for, which resolves vault slots throughfob. Neither an envoy catalog nor a credential vault belongs on a fleet node, so neither moved.
This crate holds the seam, the shared core, and the decision logic.
The machine facts this layer needs
[FloatingIpMachine] is a five-field value, not cloud's MachineConfig.
That is what makes the crate free of cloud: the adapters only ever read
name, location, region, provider and ingress_floating_ip off a
machine, so those five fields are the whole of the contract. cloud
converts at the boundary (impl From<&MachineConfig> for FloatingIpMachine)
and yubaba can build one from whatever it knows about a node without
being able to construct a MachineConfig at all — which it cannot, since a
fleet node has no .yah/infra/machines/ tree.
This mirrors, at the sovereign-ingress tier, the "external identity follows placement" property R591 names for Headscale via a Cloudflare Tunnel.