# mvirt 0.19.0

migrations: yes
wire: unchanged
rev: bb32ee94781216ce8687d9139b7624c4caa07a0d

0.19.0 — the ingress a node advertises is one it can serve

A minor, because the DNS-resolver default becomes stored state and that needs a store
migration. The release is dominated by one dataplane fix: an SDN node could advertise a
load-balancer VIP it had no way to serve, and silently drop its share of the traffic.

Load balancing (the incident this release closes)

Every SDN node installs every load balancer's frontend — the per-node slice is a
region-wide product — while the Maglev pool arrives per network over the route mesh, and a
node hosting nothing in the LB's network may not even subscribe to that scope. The
`type=LB` announce had no condition at all, so such a node still advertised the VIP,
landed in every peer's ECMP group for it, and then fail-closed on the empty pool.

Measured on our own production before the fix: 220 requests to a customer app, 52.7%
success (95% CI 46.1-59.2) — what two-way ECMP with one servable announcer predicts.
Per-flow, 53 flows reached the serving node and 53 of 53 succeeded; 53 reached the node
with no pool and 53 of 53 failed. Envoy answered 100% of what reached it. Invisible twice
over: no log line and no counter on the dropping node, and the datapath answers ICMP echo
at the frontend independently of any backend, so the VIP still pinged.

The announce now follows the effective Maglev pool, in both directions, recomputed on
every pass — the periodic re-assert enforces it, so a node that gains or loses the last
backend converges without anything being restarted. This also closes the same black hole
reached by LB_MAGLEV arena exhaustion, where a frontend that was never programmed kept
advertising. (#304)

Apps

* A deployed app's ingress route permits WebSocket upgrades. Envoy rejects an upgrade it
  has not been told about, so an app speaking WebSocket answered 403 to every upgrade and
  its owner could not change that. The route timeouts come with it, because Envoy's route
  timeout covers an upgraded stream too — announcing the upgrade without them would hand
  out connections that die on a timer. The level pass now converges these semantics on
  routes that already exist, so apps already deployed are repaired without a redeploy.
* A custom app domain is no longer written into the platform DNS zone. `irc.malte.io` on
  an app under `mvirt.run` was landing as the record `irc.malte.io.mvirt.run`, which
  resolves nothing — Bunny stores whatever relative name it is handed.

Certificates

* One hostname, one certificate. Uniqueness was enforced on the certificate name alone,
  so two certificates could cover one hostname — whichever SNI chain sorts first serves
  it, and two ACME certificates race for the same HTTP-01 authorization. The console no
  longer offers "Issue managed cert" beside a hostname whose certificate is merely not
  Issued yet, and the REST boundary refuses an overlap regardless of the caller, in both
  directions so a wildcard collides with an existing exact SAN.
* The certificate lifecycle label assumed a 90-day lifetime and read "renews in ~0 d" for
  every healthy certificate under the shortlived profile. It now mirrors the reconciler's
  rule against the certificate's own validity window.

Networking

* The resolver list a network's instances inherit is stored platform state, editable under
  Platform -> Networking, instead of a constant inside the slice compiler. The API used to
  answer `dnsServers: []` for a network whose guests resolved against a hard-coded pair,
  and changing it took an environment variable and a control-plane restart. This is the
  migration.

CLI and console

* One credential per API endpoint, and an endpoint per directory: logging in to a second
  installation no longer evicts the first, and `--api console.example/v1` reaches the
  network instead of dying as an opaque reqwest builder error. `auth whoami` and
  `auth endpoints` report which rung of the ladder chose the endpoint.
* Service-account ids are copyable, and role bindings are shown and granted in the row
  where you read them.

Upgrade notes

Control-plane host first, then nodes one at a time. This release migrates the store, so
the control-plane host does NOT roll back by booting the previous generation — it rolls
back by store restore. The cplane writes a backup immediately before applying migrations.


## Upgrading

Point your flake's `mvirt` input at this release and redeploy the control-plane
host first, then each node in turn:

```
nix run .#colmena -- apply boot --reboot --on <control-plane host>
nix run .#colmena -- apply boot --reboot --on <node>   # one at a time
```

**This release migrates the store.** Once the control plane has started, the
previous version will refuse to run against it — migrations are forward-only, so
rolling the control-plane host back means restoring a backup, with data loss back
to the backup point. The control plane writes a backup immediately before it
applies migrations; nodes roll back freely by booting the previous generation.
