# 0009. Reboot necessity is predicted, because Talos will not answer it

Status: Accepted · Date: 2026-10-25 · Design section: 5.6 (configuration reconcile)

Decided: Keep predicting reboot necessity from `pkg/fieldmeta`'s field-path table; Talos itself won't answer the question

## Context

`pkg/fieldmeta` predicts, from a small table keyed by field path, whether a
configuration change needs a node reboot. It defaults to reboot-required and
carries eight hand-written classifications. Its own author flagged the obvious
weakness: those eight are Talos domain knowledge, so a machinery bump cannot
tell us when Talos quietly changes the behaviour underneath them.

Reading machinery v1.14.3's API suggested the prediction might be unnecessary.
`ApplyConfigurationRequest.Mode` has `AUTO`, with `REBOOT` deprecated in favour
of it; `DryRun` is a bool on the same request; and the response carries `Mode`
("States which mode was actually chosen"), `ModeDetails` and `Warnings`. That
reads exactly like Talos offering to answer the question itself, per node and
per version, which would be strictly better than predicting it.

It was verified before anything was deleted, and the answer is no.

**`AUTO` is a hardcoded alias for `NO_REBOOT`.** From Talos v1.14.3,
`internal/app/machined/internal/server/v1alpha1/v1alpha1_server.go`:

```go
case machine.ApplyConfigurationRequest_AUTO:
    in.Mode = machine.ApplyConfigurationRequest_NO_REBOOT
    fallthrough
```

Nothing in that RPC ever sets `Mode = REBOOT`, and the switch never looks at
the diff. `ModeDetails` is a static string. Confirmed empirically against a
real node: three dry runs — one changing only a DNS nameserver, one changing
only the installer image tag, one applying a whole worker configuration —
returned byte-identical `Mode` and `ModeDetails`.

The real decision lives elsewhere. Each subsystem's own controller reacts to
the already-written configuration asynchronously and asks for a reboot through
a `RebootRequest` resource, which a separate controller consumes. There is no
point in the API where Talos summarises that into "this change needs a
reboot".

## Decision

Keep `pkg/fieldmeta`, and keep predicting. Its table is the only mechanism that
can answer the question where Ichor needs it answered.

Two places need it, and neither can dry-run:

- **A validating webhook on `TalosConfigPatch`, at admission time.** There may
  be no node to ask: the cluster may not be bootstrapped, the target machine
  may not exist, and admission has to answer synchronously anyway.
- **The reconciler, before it applies.** Design 5.6 step 4 requires taking the
  cluster lock, running the node-operation hook and draining *before* a
  reboot-causing apply. Learning the answer from Talos after applying is too
  late by construction.

So the design's wording — the mode is derived from the diff — stands, and now
has evidence behind it rather than being the only idea anyone had.

Two findings from the same investigation are worth acting on separately:

- **`Warnings` on the apply response is genuinely useful**, just not for this.
  It comes from `ValidateAtRuntime` and surfaces deprecated-field migration
  hints, missing cross-document configuration (etcd encryption on a control
  plane, a cluster ID or secret missing when discovery is on) and
  `KubeNodeConfig` label mismatches. The reconciler should surface it in status
  rather than dropping it.
- **Dry run is genuinely side-effect free.** Verified: a node's
  `MachineStatus` version and timestamp were unchanged after a batch of dry
  runs, not even rewritten with the same value. So it is safe to use for the
  impact preview's *validation* half, even though it cannot answer the reboot
  half.

## Consequences

- The eight hand-written entries stay, with the risk their author named: a
  Talos behaviour change will not announce itself. The mitigation is the
  default — anything not in the table is reboot-required — so the failure mode
  of a stale table is an unnecessary reboot, not a node left running a
  configuration it does not have.
- Whenever the pinned Talos version moves, those eight are worth re-reading
  against the release notes. That is a cheap, bounded review, and better than
  the alternative of trusting an API that does not answer.
- The reconciler should pass `DryRun` first for validation and warnings, then
  apply for real — two calls, with the mode decided by the table in between.

## A note for the e2e harness, found along the way

A real apply of a full worker configuration to a node in maintenance mode
starts Talos's `UnattendedInstallController`, which installs to disk and then
asks for a reboot through `RebootRequest`. On the e2e harness both test
nodes went unreachable at that point — TCP resets rather than timeouts, so the
kernel was up with nothing listening — and did not return within the test
window. Cluster pods were confirmed to have egress to `factory.talos.dev`, so
it is not obviously a network block, and it was not root-caused.

**Resolved.** It was not the install: Talos's certificate
controller blocks apid on a reachable control-plane endpoint for *worker*
nodes only, so an isolated worker never starts apid at all. A control-plane
config comes up normally. The details, and the four other harness
requirements the same investigation turned up, are in ADR 0007.

## Addendum: multi-document and registry paths

The table originally knew only the legacy `Config` paths and two standalone
kinds, so every other path fell to reboot-required. Ichor's own render hit
that: adding `TalosCluster.spec.registries` was refused with `RebootRequired`
(seen in ADR 0061's test). The v1.14.3 source was read to separate what Talos
applies live from what it does not. A rule was added only where the consumer
was found; the default is unchanged.

Ichor's default render at the v1.14 contract (control plane and worker) is the
legacy `Config` document (`machine.*`, `cluster.*`) plus the standalone kinds
DiscoveryServiceConfig, DiscoveryIdentityConfig, ResolverConfig, VolumeConfig
(EPHEMERAL), FilesystemTrimConfig, SecurityProfileConfig, HostnameConfig,
KubeClusterConfig, KubeNodeConfig, KubeletConfig, KubeNetworkConfig,
KubeAPIServerCAConfig, KubePrismConfig, and on control planes also
KubeAggregatorCAConfig, KubeAdmissionControlConfig, KubeAuditPolicyConfig,
KubeAuthenticationConfig, KubeAuthorizerConfig, KubeEtcdEncryptionConfig,
KubeServiceAccountConfig, KubeAPIServerConfig, KubeControllerManagerConfig,
KubeSchedulerConfig, KubeProxyConfig, KubeFlannelCNIConfig, KubeCoreDNSConfig.
Patches add SysctlConfig, KernelModuleConfig, VolumeConfig, UserVolumeConfig,
RawVolumeConfig and legacy `Config` fragments. Registries are injected into the
legacy `machine.registries`, not into a `RegistryMirrorConfig` document, so the
path that mattered was `machine.registries.mirrors.<host>.endpoints`.

Mode handling, for reference: `ApplyConfiguration` in
`internal/app/machined/internal/server/v1alpha1/v1alpha1_server.go` writes the
config in `NO_REBOOT` and never inspects the diff; whether anything needs a
reboot is decided by the controllers that consume the resource. Paths below are
relative to the `siderolabs/talos` module at v1.14.3 (`pkg/machinery/...` is the
machinery module).

| Document and field | No reboot? | Where in the source |
| --- | --- | --- |
| `machine.registries.mirrors` | yes | `internal/app/machined/pkg/controllers/cri/registries_config.go` builds `RegistriesConfig` from the config on every change; `internal/app/machined/pkg/controllers/files/cri_config.go` renders it to containerd's `hosts` directory via `internal/pkg/containers/cri/containerd/hosts.go` (`GenerateHosts`), which containerd reads per pull |
| `machine.registries.config` (auth and TLS), `RegistryAuthConfig`, `RegistryTLSConfig`, `RegistryMirrorConfig` | yes (CRI restarts itself; see below) | `controllers/cri/registries_config.go` lines 82-125 fold auth, mirrors and TLS from legacy and document forms into one `RegistriesConfig`; `containerd.GenerateCRIConfig` (`internal/pkg/containers/cri/containerd/containerd.go`:24-50) writes auth into the CRI config file; `controllers/files/cri_config.go`:139-180 re-renders that file on every change; `controllers/cri/service.go`:84-112 compares the file's `SpecVersion` and restarts the CRI service when it moved |
| `KubeNodeConfig.labels`, `.annotations`, `.taints`; `machine.nodeLabels`, `.nodeAnnotations`, `.nodeTaints` | yes | `controllers/k8s/node_label_spec.go`, `node_annotation_spec.go`, `node_taint_spec.go` write specs from `K8sNodeConfig()`; `node_apply.go` applies them to the Node and removes what it owned. `pkg/machinery/config/types/v1alpha1/v1alpha1_k8s_bridge.go` (`k8sNodeConfigShim`) maps the legacy fields onto the same interface |
| `cluster.allowSchedulingOnControlPlanes`, `.allowSchedulingOnMasters` | yes | read only by the taint shim above (`v1alpha1_clusterconfig.go` `ScheduleOnControlPlanes`, used only in `v1alpha1_k8s_bridge.go`) |
| `KubeAPIServerConfig`, `KubeControllerManagerConfig`, `KubeSchedulerConfig`, except `.image` | yes | `controllers/k8s/control_plane.go` reads `K8sAPIServerConfig()` etc. and regenerates the static pod; same consumer as the legacy `cluster.apiServer` rules |
| the same three kinds, `.image` | no, on purpose | see below |
| `KubeProxyConfig`, `KubeletConfig` (including `.image`) | unknown, kept default | see below |

(`controllers/` is `internal/app/machined/pkg/controllers/`.)

**The Kubernetes component images are deliberately not no-reboot.**
`KubeAPIServerConfig.image`, `KubeControllerManagerConfig.image`,
`KubeSchedulerConfig.image`, `KubeProxyConfig.image` and `KubeletConfig.image`
would apply live in Talos, but a safe rule makes the machine controller apply
all nodes the moment `spec.kubernetesVersion` changes, which breaks the
one-node-at-a-time order ADR 0057's executor provides (it applies the image
fields itself with an explicit no-reboot apply). The explicit `.image` entries
in `pkg/fieldmeta` carve them out of their document's rule, and a test pins
all five. The legacy `cluster.apiServer.image`, `cluster.controllerManager.image`
and `cluster.scheduler.image` are still covered by the older `cluster.*` safe
rules, so on a pre-1.14 contract the same hazard exists; it predates this
addendum and is left for its own decision.

**Read but not verified, so left at the default:** `KubeProxyConfig` (the open
question about the kube-proxy DaemonSet stands), `KubeletConfig` fields other
than the image and `machine.kubelet.*` (kubelet restart behaviour not traced),
`KubeNodeConfig.nodeIP`, `ResolverConfig.hostDNS`, `HostnameConfig`,
`DiscoveryServiceConfig`/`DiscoveryIdentityConfig`, `SecurityProfileConfig`,
`KubeClusterConfig`, `KubeNetworkConfig`, the CA, admission, audit,
authentication, authorizer, etcd-encryption, service-account, Prism, Flannel
and CoreDNS kinds, `VolumeConfig`/`FilesystemTrimConfig` beyond
`provisioning`. Ichor's inputs never vary most of these after the first
apply.

## Addendum: an unchanged hostname needs no reboot

fieldmeta stays path-only, so `HostnameConfig.*` and `machine.network.hostname` are reboot-required. `assessChange` (`internal/controller/hostname_noop.go`) drops those paths when `spec.hostname` equals the node's current Nodename, so the release that began rendering `spec.hostname` does not demand a reboot of every existing node; a real rename stays reboot-required and its message says the Kubernetes node keeps its old name until the reboot.

## Addendum: registry auth and TLS apply without a reboot

The first addendum kept registry auth at reboot-required because machinery's doc comment on `RegistryConfig.RegistryAuth` (`pkg/machinery/config/types/v1alpha1/v1alpha1_types.go`:2058) says "changes to the registry auth will not be picked up by the CRI containerd plugin without a reboot". The v1.14.3 machined source shows that comment is stale. Auth is written into the CRI config file (`GenerateCRIConfig`, `containerd.go`:24-50), which containerd reads only at start; but `ServiceController.reconcile` (`internal/app/machined/pkg/controllers/cri/service.go`:84-112) restarts the CRI service whenever the `SpecVersion` of that file (or the base runtime spec) changes, and `CRIConfigController` (`controllers/files/cri_config.go`:139-180) re-renders it on every registries change. Running pods keep running while containerd restarts. TLS and mirrors become hosts.toml files and need no restart at all. So `machine.registries.config`, `RegistryAuthConfig`, `RegistryTLSConfig` and `RegistryMirrorConfig` are now safe (the legacy host key contains dots, so the rule is the whole subtree). Unverified: Talos versions older than v1.14.3 were not read; Ichor targets v1.14.
