Skip to content

Commit e64dfd2

Browse files
committed
fix(charts): rewrite v1.12 multi-doc network renderer for full link coverage
The previous multi-doc renderer resolved a single $defaultLinkName via talm.discovered.default_link_name_by_gateway and emitted exactly one network document for it: BondConfig, VLANConfig, or LinkConfig depending on the gateway link kind. Every other link on the node — secondary physical NIC, VLAN sub-interface on a non-gateway parent, additional bond — was silently dropped from the rendered config. Replace the single-link block with a range over talm.discovered.configurable_link_names, emitting the appropriate document per link kind (bond / vlan / physical). The IPv4 default-route gateway is placed only on the link that carries it; every other link gets addresses without a default route. MTU is surfaced on each document when discovery reports a value, so non-default-MTU links (jumbo frames, GRE) survive a re-render. Also harden three coverage edges that the simple range exposes: - Filter bond slaves out of configurable_link_names. Slaves match the physical-NIC regex but are members of a bond master via spec.slaveKind; emitting a standalone LinkConfig for a slave alongside the master's BondConfig produces conflicting declarations Talos rejects on controller convergence. - Skip kind=bridge in the renderer. The chart does not yet emit BridgeConfig, so falling through to LinkConfig would land a wrong-kind document. The branch is reserved for a future PR that adds bridge support. - Gate every BondMaster sub-field on its presence. A bond returned with a partial or absent bondMaster (real Talos shape on freshly- created bonds before the master controller fills the spec) used to render bondMode: <nil> and break the parse. Surface a hard-fail in the multi-doc renderer when the running MachineConfig still carries non-empty machine.network.interfaces[]. The renderer cannot translate those entries today, and silently dropping them on the first re-render after a chart upgrade from the legacy schema would erase user-declared interfaces, addresses, and VLANs. The fail message points the operator at the concrete migration path: move the interfaces into per-node body overlays as v1.12 typed documents, or pin templateOptions.talosVersion to v1.11 until the translator lands. README updated to describe the per-link emission, the bond-slave filter, and the upgrade-from-legacy guardrail with the fail message text and migration path. Together this closes the multi-doc renderer coverage gaps: single-link-only emission, dropped non-gateway VLAN sub-interfaces, silent loss of legacy machine.network.interfaces during upgrade, and missing MTU on every multi-doc document kind. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
1 parent 9ac5eb8 commit e64dfd2

4 files changed

Lines changed: 265 additions & 186 deletions

File tree

README.md

Lines changed: 16 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,15 @@ Talm is just like Helm, but for Talos Linux
88

99
While developing Talm, we aimed to achieve the following goals:
1010

11-
- **Automatic Discovery**: In a bare-metal environment, each server may vary
12-
slightly in aspects such as disks and network interfaces.
13-
Talm enables discovery of node information, which is then used to generate patches.
11+
- **Automatic Discovery**: In a bare-metal environment, each server may vary slightly in aspects such as disks and network interfaces. Talm enables discovery of node information, which is then used to generate patches.
1412

15-
- **Ease of Customization**: You can customize templates to create your unique
16-
configuration based on your environment. The templates use the standard
17-
Go templates syntax, enhanced with widely-known Helm templating logic.
13+
- **Ease of Customization**: You can customize templates to create your unique configuration based on your environment. The templates use the standard Go templates syntax, enhanced with widely-known Helm templating logic.
1814

19-
- **GitOps Friendly**: The patches generated do not contain sensitive data,
20-
allowing them to be stored in Git in an unencrypted, open format. For scenarios
21-
requiring complete configurations, the `--full` option allows the obtain
22-
a complete config that can be used for matchbox and other solutions.
15+
- **GitOps Friendly**: The patches generated do not contain sensitive data, allowing them to be stored in Git in an unencrypted, open format. For scenarios requiring complete configurations, the `--full` option allows the obtain a complete config that can be used for matchbox and other solutions.
2316

24-
- **Simplicity of Use**: You no longer need to pass connection options for each
25-
specific server; they are saved along with the templating results into
26-
a separate file. This allows you to easily apply one or multiple files in batch
27-
using a syntax similar to `kubectl apply -f node1.yaml -f node2.yaml`.
17+
- **Simplicity of Use**: You no longer need to pass connection options for each specific server; they are saved along with the templating results into a separate file. This allows you to easily apply one or multiple files in batch using a syntax similar to `kubectl apply -f node1.yaml -f node2.yaml`.
2818

29-
- **Compatibility with talosctl**: We strive to maintain compatibility with the upstream
30-
project in patches and configurations. The configurations you obtain can be used
31-
with the official tools like talosctl and Omni.
19+
- **Compatibility with talosctl**: We strive to maintain compatibility with the upstream project in patches and configurations. The configurations you obtain can be used with the official tools like talosctl and Omni.
3220

3321

3422
## Installation
@@ -52,14 +40,7 @@ curl -sSL https://github.com/cozystack/talm/raw/refs/heads/main/hack/install.sh
5240

5341
### Windows
5442

55-
Windows is supported. Download the `talm-windows-*.zip` archive from the
56-
[releases page](https://github.com/cozystack/talm/releases/latest) and
57-
extract `talm.exe`. On Windows, template paths passed to the `-t` /
58-
`--template` flag accept either `\` or `/` separators, so
59-
`-t templates\controlplane.yaml` and `-t templates/controlplane.yaml`
60-
are equivalent. Other path flags (`--talosconfig`, `-f` / `--file`)
61-
are delegated to the underlying OS file loader and follow standard
62-
Windows path rules.
43+
Windows is supported. Download the `talm-windows-*.zip` archive from the [releases page](https://github.com/cozystack/talm/releases/latest) and extract `talm.exe`. On Windows, template paths passed to the `-t` / `--template` flag accept either `\` or `/` separators, so `-t templates\controlplane.yaml` and `-t templates/controlplane.yaml` are equivalent. Other path flags (`--talosconfig`, `-f` / `--file`) are delegated to the underlying OS file loader and follow standard Windows path rules.
6344

6445
## Getting Started
6546

@@ -70,28 +51,7 @@ cd newcluster
7051
talm init -p cozystack -N myawesomecluster
7152
```
7253

73-
Edit `values.yaml` to set your cluster's control-plane endpoint. This
74-
is the URL every node's kubelet and kube-proxy will dial. The chart
75-
leaves it empty on purpose so a missed override fails loudly instead
76-
of silently embedding a placeholder. For cozystack VIP setups set
77-
`endpoint` and `floatingIP` together (same IP, single shared VIP);
78-
for single-node clusters use that node's routable IP and leave
79-
`floatingIP` blank; for multi-node with an external load balancer
80-
use the LB URL and leave `floatingIP` blank. When the VIP must sit
81-
on a link that does not yet exist on the live system at first apply
82-
(typically a VLAN sub-interface), set `vipLink` to that link name —
83-
the chart pins `Layer2VIPConfig.link` to it instead of the default-
84-
gateway link that discovery would otherwise pick, and emits the
85-
document even on a totally fresh node where no default-gateway link
86-
has been discovered yet. The chart does not auto-emit a `LinkConfig`
87-
or `VLANConfig` for the override link; the operator is responsible
88-
for ensuring the link comes up, typically by adding a `LinkConfig`
89-
or `VLANConfig` for that link to the per-node body overlay alongside
90-
`vipLink`. Subnet-selector fields
91-
(`kubelet.validSubnets`, `etcd.advertisedSubnets`) are derived
92-
automatically from the node's default-gateway-bearing link, so no
93-
override is needed unless you have a multi-homed node that requires
94-
a specific subnet pinned.
54+
Edit `values.yaml` to set your cluster's control-plane endpoint. This is the URL every node's kubelet and kube-proxy will dial. The chart leaves it empty on purpose so a missed override fails loudly instead of silently embedding a placeholder. For cozystack VIP setups set `endpoint` and `floatingIP` together (same IP, single shared VIP); for single-node clusters use that node's routable IP and leave `floatingIP` blank; for multi-node with an external load balancer use the LB URL and leave `floatingIP` blank. When the VIP must sit on a link that does not yet exist on the live system at first apply (typically a VLAN sub-interface), set `vipLink` to that link name — the chart pins `Layer2VIPConfig.link` to it instead of the default-gateway link that discovery would otherwise pick, and emits the document even on a totally fresh node where no default-gateway link has been discovered yet. The chart does not auto-emit a `LinkConfig` or `VLANConfig` for the override link; the operator is responsible for ensuring the link comes up, typically by adding a `LinkConfig` or `VLANConfig` for that link to the per-node body overlay alongside `vipLink`. Subnet-selector fields (`kubelet.validSubnets`, `etcd.advertisedSubnets`) are derived automatically from the node's default-gateway-bearing link, so no override is needed unless you have a multi-homed node that requires a specific subnet pinned.
9555

9656
Boot Talos Linux node, let's say it has address `192.0.2.4`. Then:
9757

@@ -156,9 +116,7 @@ cluster:
156116
endpoint: https://192.0.2.4:6443
157117
```
158118
159-
> **Note:** The output format depends on the Talos version configured in `Chart.yaml` (`templateOptions.talosVersion`) or via the `--talos-version` CLI flag.
160-
> For Talos < v1.12, the output is a single YAML document with `machine.network` and `machine.registries` sections (as shown above).
161-
> For Talos >= v1.12, the output uses the multi-document format with separate typed documents instead of the deprecated monolithic fields. `HostnameConfig`, `ResolverConfig` and a network interface document (`LinkConfig`, `BondConfig`, or `VLANConfig` — depending on topology) are always emitted; `Layer2VIPConfig` appears on controlplane nodes when `floatingIP` is set; `RegistryMirrorConfig` is emitted only by the cozystack chart.
119+
> **Note:** The output format depends on the Talos version configured in `Chart.yaml` (`templateOptions.talosVersion`) or via the `--talos-version` CLI flag. For Talos < v1.12, the output is a single YAML document with `machine.network` and `machine.registries` sections (as shown above). For Talos >= v1.12, the output uses the multi-document format with separate typed documents instead of the deprecated monolithic fields. `HostnameConfig` and `ResolverConfig` are always emitted; one network interface document is emitted per configurable link on the node (`LinkConfig` for physical NICs, `BondConfig` for bond masters, `VLANConfig` for VLAN sub-interfaces) — multi-NIC nodes therefore produce one document per NIC, not one document total. The link carrying the IPv4 default route gets the gateway entry on its document; every other link is emitted gateway-less. Both IPv4 and IPv6 global-scope addresses on a link are surfaced in its document. Bond slaves are filtered out so they do not collide with the master's `BondConfig`. Bridges are deliberately not auto-emitted as `BridgeConfig` yet — a non-gateway bridge is skipped (declare it via a per-node body overlay if needed); a bridge that carries the default route fails the render with a clear migration hint. The operator-declared `floatingIP` is stripped from per-link addresses so the VIP currently held by a leader does not leak into the static `LinkConfig`. `Layer2VIPConfig` appears on controlplane nodes when `floatingIP` is set; `RegistryMirrorConfig` is emitted only by the cozystack chart.
162120
163121
> **Version compatibility (`templateOptions.talosVersion` / `--talos-version`).** This setting must match the **Talos version actually running on the target node** — i.e. the maintenance ISO/PXE the node booted from for `apply -i`, or the installed Talos for an authenticated apply. It is **not** the same as `install.image`, which only controls what gets written to disk after a successful apply. When the configured contract is newer than the running binary, machinery injects fields (e.g. `machine.install.grubUseUKICmdline` from v1.12) that the running parser does not know, and the apply fails on the node side with `failed to parse config: unknown keys found during decoding: ...`. `talm apply` runs a best-effort pre-flight check against the running version and prints a `warning: pre-flight: ...` line with a hint when it detects this mismatch; if the warning is missed, the same hint is appended to the apply error. Either reboot the node into a maintenance image that matches the configured contract, or lower `templateOptions.talosVersion` / `--talos-version` to match what is running.
164122
@@ -182,67 +140,21 @@ Re-template and update generated file in place (this will overwrite it):
182140
talm template -f nodes/node1.yaml -I
183141
```
184142

185-
> **Per-node patches inside node files.** A node file can carry Talos config
186-
> below its modeline (for example, a custom `hostname`, secondary
187-
> interfaces with `deviceSelector`, VIP placement, or extra etcd args).
188-
> When `talm apply -f node.yaml` runs the template-rendering branch, that
189-
> body is applied as a strategic merge patch on top of the rendered
190-
> template before the result is sent to the node — so per-node fields
191-
> survive even when the template auto-generates conflicting values
192-
> (e.g. `hostname: talos-XXXXX`).
143+
> **Per-node patches inside node files.** A node file can carry Talos config below its modeline (for example, a custom `hostname`, secondary interfaces with `deviceSelector`, VIP placement, or extra etcd args). When `talm apply -f node.yaml` runs the template-rendering branch, that body is applied as a strategic merge patch on top of the rendered template before the result is sent to the node — so per-node fields survive even when the template auto-generates conflicting values (e.g. `hostname: talos-XXXXX`).
193144
>
194-
> **Talos v1.12+ caveat.** The multi-document output format introduced
195-
> in v1.12 splits network configuration into typed documents
196-
> (`LinkConfig`, `BondConfig`, `VLANConfig`, `Layer2VIPConfig`,
197-
> `HostnameConfig`, `ResolverConfig`). Legacy node-body fields under
198-
> `machine.network.interfaces` have no safe 1:1 mapping to those types,
199-
> so the multi-doc path does not translate them — if you target a
200-
> v1.12+ Talos node, pin per-node network settings by patching the
201-
> typed resources (e.g. a `LinkConfig` document below the modeline)
202-
> rather than legacy `machine.network.interfaces`. Fields outside the
203-
> network area (`machine.network.hostname` via `HostnameConfig`,
204-
> `machine.install.disk`, extra etcd args, etc.) still merge as
205-
> expected.
145+
> **Talos v1.12+ caveat.** The multi-document output format introduced in v1.12 splits network configuration into typed documents (`LinkConfig`, `BondConfig`, `VLANConfig`, `Layer2VIPConfig`, `HostnameConfig`, `ResolverConfig`). Legacy node-body fields under `machine.network.interfaces` have no safe 1:1 mapping to those types and the chart cannot translate them yet — pin per-node network settings by patching the typed resources (e.g. a `LinkConfig` document below the modeline) rather than legacy `machine.network.interfaces`. Fields outside the network area (`machine.network.hostname` via `HostnameConfig`, `machine.install.disk`, extra etcd args, etc.) still merge as expected.
206146
>
207-
> **One body, one node.** A non-empty body is a per-node pin, so the
208-
> modeline for that file must target exactly one node. `talm apply`
209-
> refuses a multi-node modeline when the body is non-empty; modeline-
210-
> only files (no body) are still allowed and drive the same rendered
211-
> template on every listed target.
147+
> **Upgrade-from-legacy guardrail.** Nodes originally bootstrapped on a chart that emitted the legacy schema still carry `machine.network.interfaces[]` in their running `MachineConfig`. On v1.12 multi-doc rendering the chart cannot reconstruct equivalent typed documents from those entries automatically, and silently dropping them on the next apply would erase the user's network declarations. The renderer therefore fails the render with `talm: the multi-doc renderer cannot translate legacy machine.network.interfaces[] from the running MachineConfig...`, spelling out the migration path: move the interfaces, vlans, and addresses into per-node body overlays as v1.12 typed documents (`LinkConfig`, `VLANConfig`, `BondConfig`, `RouteConfig`) before re-running `talm apply`, or pin `templateOptions.talosVersion: "v1.11"` in `Chart.yaml` until the translator lands.
212148
>
213-
> **Idempotent applies.** Repeated `talm apply` runs against an
214-
> already-configured node do not duplicate entries. Before the strategic
215-
> merge runs, the engine prunes from the body every primitive-list
216-
> entry the rendered template already carries (e.g. certSANs,
217-
> nameservers, validSubnets). For object arrays the upstream patcher
218-
> merges by identity (machine.network.interfaces by `interface:` or
219-
> `deviceSelector:`, vlans by `vlanId:`, apiServer admissionControl by
220-
> `name:`), the prune descends into matched pairs and dedupes the inner
221-
> primitive lists too — so re-applying after `talm template -I` does not
222-
> double interface addresses, vlan addresses, or admission-control
223-
> exemption namespaces. For object arrays without an upstream identity
224-
> merge (extraVolumes, kernel.modules, wireguard.peers, ...), body items
225-
> that deep-equal a rendered counterpart are dropped, covering the
226-
> dominant full-restate case. Fields tagged `merge:"replace"` upstream
227-
> are passed through verbatim — pruning them would let the upstream
228-
> replace silently drop the rendered entries on a partial edit. This
229-
> covers v1alpha1 root paths `cluster.network.podSubnets`,
230-
> `cluster.network.serviceSubnets`, `cluster.apiServer.auditPolicy`,
231-
> and the typed `NetworkRuleConfig` paths `ingress` and
232-
> `portSelector.ports`.
149+
> **One body, one node.** A non-empty body is a per-node pin, so the modeline for that file must target exactly one node. `talm apply` refuses a multi-node modeline when the body is non-empty; modeline-only files (no body) are still allowed and drive the same rendered template on every listed target.
233150
>
234-
> `talm template -f node.yaml` (with or without `-I`) does **not** apply
235-
> the same overlay: its output is the rendered template plus the modeline
236-
> and the auto-generated warning, byte-identical to what the template
237-
> alone would produce. Routing it through the patcher would drop every
238-
> YAML comment (including the modeline) and re-sort keys, breaking
239-
> downstream commands that read the file back. Use `apply --dry-run` if
240-
> you want to preview the exact bytes that will be sent to the node.
151+
> **Idempotent applies.** Repeated `talm apply` runs against an already-configured node do not duplicate entries. Before the strategic merge runs, the engine prunes from the body every primitive-list entry the rendered template already carries (e.g. certSANs, nameservers, validSubnets). For object arrays the upstream patcher merges by identity (machine.network.interfaces by `interface:` or `deviceSelector:`, vlans by `vlanId:`, apiServer admissionControl by `name:`), the prune descends into matched pairs and dedupes the inner primitive lists too — so re-applying after `talm template -I` does not double interface addresses, vlan addresses, or admission-control exemption namespaces. For object arrays without an upstream identity merge (extraVolumes, kernel.modules, wireguard.peers, ...), body items that deep-equal a rendered counterpart are dropped, covering the dominant full-restate case. Fields tagged `merge:"replace"` upstream are passed through verbatim — pruning them would let the upstream replace silently drop the rendered entries on a partial edit. This covers v1alpha1 root paths `cluster.network.podSubnets`, `cluster.network.serviceSubnets`, `cluster.apiServer.auditPolicy`, and the typed `NetworkRuleConfig` paths `ingress` and `portSelector.ports`.
152+
>
153+
> `talm template -f node.yaml` (with or without `-I`) does **not** apply the same overlay: its output is the rendered template plus the modeline and the auto-generated warning, byte-identical to what the template alone would produce. Routing it through the patcher would drop every YAML comment (including the modeline) and re-sort keys, breaking downstream commands that read the file back. Use `apply --dry-run` if you want to preview the exact bytes that will be sent to the node.
241154
242155
## Using talosctl commands
243156

244-
Talm offers a similar set of commands to those provided by talosctl.
245-
However, you can specify the --file option for them.
157+
Talm offers a similar set of commands to those provided by talosctl. However, you can specify the --file option for them.
246158

247159
For example, to run a dashboard for three nodes:
248160

0 commit comments

Comments
 (0)