You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The node Project leaves fleet-infra and is delivered from the estate repository, in the order #381 sets.
Add a nodeAgents catalog to the estate's Platform document, with locked images for the MetalLB speaker, node-exporter, the GPU device plugin and DCGM, and add node to the handover ledger. Today the estate platform names none, so a render that derives Node Agents is refused with E_NO_NODE_AGENT_POLICY.
The estate-side changes are recorded in the estate repository's HANDOVER.md.
Acceptance criteria
The render's diff against the Project's hand-written objects in fleet-infra is clean, or each difference is explained.
The Project's Reconcile Units report Ready.
A consumer-visible value checks out: Each GPU node advertises its resource, and every node's metrics are scraped.
One change flips the handover ledger entry and deletes the hand-written objects, the descriptor and the drift-check entry.
Decisions: in this ticket's body and in Rewrite chapter 60 to rules, and write the handover and setup runbooks #381's resume note, dated, including 2026-10-27's: Vault moves by raft snapshot and restore, observability starts fresh, Grafana goes in observability, and Garage's volume splits at handover.
Gotchas: keep every live claim name the body lists. A live handover step is the maintainer's go-ahead, never an agent's.
Prepared, 2026-10-29: four draft PRs, two blockers in deploy-kit
The window is in JorisJonkers-dev/estate docs/runbooks/node-handover.md (on #261's branch).
The PRs, in merge order
JorisJonkers-dev/fleet-infra#565, any time before the window. traefik-lan stops naming the pool enschede-lan and asks for its address alone. Otherwise the new controller clears the LAN ingress address, because the render's pool is named lan-edge.
metallb.io/v1beta1 joins bootstrap.crds, and bootstrap/cluster.sh gains a stage that adopts MetalLB v0.15.3's CRDs with bgppeers' conversion webhook dropped.
nodeAgents memory is sized to live use: dcgm-exporter 1Gi, since it uses about 410Mi and was OOM-killed at 256Mi. The speaker and controller get 128Mi.
Every lock digest matches the live pods' imageID.
Between 3 and 6, by hand:
stop the old controller and delete metallb-webhook-configuration, which fails closed and would refuse the render's pools;
run cluster.sh stage 5;
delete the old device plugin, so two plugins never share nvidia-gpu.sock.
After 6, by hand:
delete metallb-speaker; the new speakers wait on host port 7472, so each node's gap is seconds;
then node-exporter (port 9100);
then dcgm-exporter, once gpu-metrics is Ready.
Order against utility
node goes after utility's window, and #261 is updated from main after JorisJonkers-dev/estate#248 merges, so the render holds lan-dns. Utility also has to have deleted adguard-ha-b and moved the router to .98 alone: the render has no pool for .97.
The pool move needs no overlap trick. Each controller reads only its own namespace's pools, and the old webhook is deleted before the flip. The new controller finds .98 and .99 already assigned, inside its own lan-dns and lan-edge, and keeps both. Only the speakers change hands, node by node.
Open blockers
The rendered l4-speaker and l4-controller cannot start on MetalLB 0.15.3 #729 (bug), blocking. The rendered l4-speaker and l4-controller do not start on MetalLB 0.15.3. I reproduced it on a throwaway local k3s, from the estate's own render. The speaker has no METALLB_POD_NAME and exits. Neither ClusterRole has the reads 0.15.3 always watches (configurationstates, secrets, configmaps, and the controller's nodes). With those added by hand, both ran. Needs a deploy-kit release and toolkit.yml moved to it.
Every difference is explained in #261's body and the runbook. In short:
every agent is a new DaemonSet in node-system;
the speaker runs on the five Enschede nodes, not frankfurt-contabo-1, in native L2 mode with no excludel2 list;
pools are lan-edge and lan-dns, both autoAssign: false;
the device plugin has no config-manager sidecar or MPS;
dcgm-exporter uses the image's counters, a superset of the chart's;
node-exporter has no textfile collector or probes.
Validated with deploy-kit 1.2.0, as toolkit.yml names it: CI's validate, every release-matrix entry, a negative E_NO_NODE_AGENT_POLICY and a local deploy-kit compose. Nothing is merged.
apps-node is Ready. The new speakers joined one memberlist and announce both LAN addresses; DNS and LAN ingress answered from the LAN throughout the swap.
Both GPU nodes advertise their cards through gpu-device; jellyfin and immich kept running.
Prometheus scrapes node-metrics on all six nodes and gpu-metrics on both GPU nodes, under the old job names.
The old path's objects are deleted: the speakers, controller, webhook, device plugin, dcgm-exporter, node-exporter, their releases, namespaces and RBAC.
Found in the window:
The new speakers started beside the old ones instead of waiting for a host port, so for about an hour both sets announced the same addresses. Nothing broke, but the runbook's assumption that they wait was wrong.
The speakers' memberlist runs over the nodes' Tailscale addresses, and the cordoned Pis keep suspecting each other failed. Election stays on the GPU nodes, but failover is less reliable than the local test showed.
On the T1000, the new gpu-metrics could not start until the old dcgm-exporter released DCGM's profiling module, so the runbook's step 10 order deadlocks there; deleting the old exporter first is the way out.
Activity
ExtraToast commentedon Oct 27, 2026
Resume here
ExtraToast commentedon Oct 29, 2026
#380 is closed: delivery,
_canaryand_estateare on the 1.0 contract (evidence in #380 (comment)).ExtraToast commentedon Oct 29, 2026
Prepared, 2026-10-29: four draft PRs, two blockers in deploy-kit
The window is in JorisJonkers-dev/estate
docs/runbooks/node-handover.md(on #261's branch).The PRs, in merge order
enschede-lanand asks for its address alone. Otherwise the new controller clears the LAN ingress address, because the render's pool is namedlan-edge.lan-dnsandapps-metallb-lan-dns.prune: disabled.metallb,nvidia-device-pluginanddcgm-exporterHelmReleases.prune: falseon both MetalLB pool Kustomizations.helm.sh/resource-policy: keepon node-exporter's four kube-prometheus-stack objects.nodeExporter.enabled: false.nvidia-cdiRuntimeClass stays, moved under the same name.node-systemasjob="node-exporter"andjob="dcgm-exporter".nodemoves toestate.metallb.io/v1beta1joinsbootstrap.crds, andbootstrap/cluster.shgains a stage that adopts MetalLB v0.15.3's CRDs withbgppeers' conversion webhook dropped.nodeAgentsmemory is sized to live use: dcgm-exporter 1Gi, since it uses about 410Mi and was OOM-killed at 256Mi. The speaker and controller get 128Mi.Between 3 and 6, by hand:
metallb-webhook-configuration, which fails closed and would refuse the render's pools;cluster.shstage 5;nvidia-gpu.sock.After 6, by hand:
metallb-speaker; the new speakers wait on host port 7472, so each node's gap is seconds;gpu-metricsis Ready.Order against utility
node goes after utility's window, and #261 is updated from
mainafter JorisJonkers-dev/estate#248 merges, so the render holdslan-dns. Utility also has to have deletedadguard-ha-band moved the router to .98 alone: the render has no pool for .97.The pool move needs no overlap trick. Each controller reads only its own namespace's pools, and the old webhook is deleted before the flip. The new controller finds .98 and .99 already assigned, inside its own
lan-dnsandlan-edge, and keeps both. Only the speakers change hands, node by node.Open blockers
l4-speakerandl4-controllerdo not start on MetalLB 0.15.3. I reproduced it on a throwaway local k3s, from the estate's own render. The speaker has noMETALLB_POD_NAMEand exits. Neither ClusterRole has the reads 0.15.3 always watches (configurationstates,secrets,configmaps, and the controller'snodes). With those added by hand, both ran. Needs a deploy-kit release andtoolkit.ymlmoved to it.uuidGPU ids. The hosts' NixOS CDI specification namesindex, which is why live setsdeviceIDStrategy: index. Either the render saysindex, or nix-config switches the specification touuid.NvidiaCdiSpecMissing,NvidiaCdiStateStaleand the module-mismatch alert go silent until it lands.apps-core. Thenvidia-cdiRuntimeClass is neither rendered nor in the bootstrap set.Render against live
Every difference is explained in #261's body and the runbook. In short:
node-system;frankfurt-contabo-1, in native L2 mode with no excludel2 list;lan-edgeandlan-dns, bothautoAssign: false;Validated with deploy-kit 1.2.0, as
toolkit.ymlnames it: CI's validate, every release-matrix entry, a negativeE_NO_NODE_AGENT_POLICYand a localdeploy-kit compose. Nothing is merged.feat: hand the node Project over to the estate path (#261)
ExtraToast commentedon Oct 30, 2026
node is on the estate path, delivered by estate 0.35.0 on deploy-kit 1.5.0.
bootstrap/cluster.shadopted MetalLB's CRDs.apps-nodeis Ready. The new speakers joined one memberlist and announce both LAN addresses; DNS and LAN ingress answered from the LAN throughout the swap.gpu-device; jellyfin and immich kept running.node-metricson all six nodes andgpu-metricson both GPU nodes, under the old job names.Found in the window:
gpu-metricscould not start until the old dcgm-exporter released DCGM's profiling module, so the runbook's step 10 order deadlocks there; deleting the old exporter first is the way out.