From ae0bc3042c51b77e0214076c036bdc6f92cc6448 Mon Sep 17 00:00:00 2001 From: Kirill Ilin Date: Wed, 24 Jun 2026 14:32:01 +0500 Subject: [PATCH 01/10] chore(deps): bump Talos to v1.14.0 Tracks upstream Talos v1.14.0 for both the main module and pkg/machinery, so talm recognizes the v1.14 machine-config documents, in particular KubeEtcdEncryptionConfig, which the earlier machinery rejected as not registered. v1.14 reshaped what talm consumes: the talosctl client wrappers became a ClientFactory that refuses to build without nodes, cluster.Name and cluster.Endpoint moved to K8sClusterConfig, helpers.FailIfMultiNodes and helpers.ForEachResource left the exported helpers, config/generate now errors on an empty Kubernetes version instead of defaulting it, and --insecure moved from a persistent to a local flag on the meta command. talm carries local equivalents for the wrappers and the two helpers. Three flags disappear with the release, each with a replacement: apply's --mode=reboot (use --mode=auto, which the node promotes to a reboot when the change needs one), and upgrade's and reset's --insecure (boot a maintenance image and apply a fresh config instead). Shell completion now reads the mode values back from the flag, so it cannot advertise one the flag rejects. Co-authored-by: Andrei Kvapil Signed-off-by: Kirill Ilin Signed-off-by: Andrei Kvapil Signed-off-by: Aleksei Sviridkin Assisted-by: LLM --- docs/manual-test-plan.md | 47 ++- docs/operations/safety-gates.md | 2 +- docs/operations/talosctl-commands.md | 20 ++ docs/reference/apply.md | 44 +-- docs/reference/containers.md | 6 +- docs/reference/debug.md | 2 +- docs/reference/edit.md | 12 +- docs/reference/get.md | 13 +- docs/reference/image.md | 2 +- docs/reference/image_cache-cert-gen.md | 2 +- docs/reference/image_cache-create.md | 4 +- docs/reference/image_cache-serve.md | 2 +- docs/reference/image_integration.md | 4 +- docs/reference/image_k8s-bundle.md | 12 +- docs/reference/image_list.md | 2 +- docs/reference/image_pull.md | 2 +- docs/reference/image_remove.md | 2 +- docs/reference/image_talos-bundle.md | 2 +- docs/reference/logs.md | 12 +- docs/reference/memory.md | 8 +- docs/reference/meta.md | 7 +- docs/reference/meta_delete.md | 1 - docs/reference/meta_write.md | 1 - docs/reference/reset.md | 1 - docs/reference/restart.md | 6 +- docs/reference/service.md | 6 +- docs/reference/stats.md | 6 +- docs/reference/support.md | 21 +- docs/reference/template.md | 2 +- docs/reference/upgrade.md | 2 +- docs/reference/version.md | 11 +- docs/reference/wipe.md | 4 + docs/reference/wipe_disk.md | 11 +- docs/reference/wipe_lv.md | 41 +++ docs/reference/wipe_md.md | 43 +++ docs/reference/wipe_pv.md | 43 +++ docs/reference/wipe_vg.md | 44 +++ go.mod | 251 +++++++------- go.sum | 327 +++++++++--------- pkg/commands/apply.go | 41 ++- pkg/commands/apply_test.go | 16 +- pkg/commands/client_wrappers_test.go | 157 +++++++++ pkg/commands/completion.go | 31 +- pkg/commands/completion_test.go | 36 +- pkg/commands/contract_init_ux_test.go | 40 +++ pkg/commands/init.go | 14 +- pkg/commands/preflight_apply_safety_test.go | 4 +- pkg/commands/preflight_upgrade_verify_test.go | 31 +- pkg/commands/root.go | 123 ++++++- pkg/commands/skip_verify_test.go | 8 +- pkg/commands/talosconfig.go | 5 +- pkg/commands/talosctl_wrapper_test.go | 43 ++- pkg/commands/template.go | 1 + pkg/commands/upgrade_handler.go | 24 +- pkg/engine/contract_lookup_classify_test.go | 2 +- pkg/engine/contract_lookup_retry_test.go | 2 +- pkg/engine/engine.go | 45 ++- pkg/engine/lookup_classify.go | 6 +- pkg/engine/render_test.go | 5 +- pkg/engine/talos_helpers.go | 146 ++++++++ pkg/engine/talos_helpers_test.go | 150 ++++++++ 61 files changed, 1426 insertions(+), 532 deletions(-) create mode 100644 docs/reference/wipe_lv.md create mode 100644 docs/reference/wipe_md.md create mode 100644 docs/reference/wipe_pv.md create mode 100644 docs/reference/wipe_vg.md create mode 100644 pkg/commands/client_wrappers_test.go create mode 100644 pkg/engine/talos_helpers.go create mode 100644 pkg/engine/talos_helpers_test.go diff --git a/docs/manual-test-plan.md b/docs/manual-test-plan.md index 4e415200..22580f80 100644 --- a/docs/manual-test-plan.md +++ b/docs/manual-test-plan.md @@ -1002,7 +1002,7 @@ The Phase 1 walker validates the syntactic shape of net-addr fields in three v1a | Dry-run shows preview | `talm apply --dry-run -f node.yaml` | Phase 2A runs; this is the "show me what would change" contract | | `--mode=no-reboot` | `talm apply --mode=no-reboot -f node.yaml` | Phase 2A runs | | `--mode=auto` | `talm apply --mode=auto -f node.yaml` | Phase 2A runs | -| `--mode=reboot` | `talm apply --mode=reboot -f node.yaml` | Phase 2A runs (preview is read-only and shows what the reboot will activate) | +| `--mode=reboot` | `talm apply --mode=reboot -f node.yaml` | Rejected since Talos v1.14: `invalid argument "reboot" for "-m, --mode" flag`. Upstream dropped the spelling; use `--mode=auto`, which the node promotes to a reboot when the change needs one | | `--mode=staged` | `talm apply --mode=staged -f node.yaml` | Phase 2A runs (operator still wants to see what got staged) | | `--mode=try` | `talm apply --mode=try -f node.yaml` | Phase 2A runs (mirrors --mode=auto from the preview's perspective) | | Insecure path | `talm apply -i -f node.yaml` (where chart can render offline) | `talm: drift verification unavailable on maintenance connection`; no block | @@ -1049,7 +1049,6 @@ Default off until the Talos-mutated-field allowlist lands. Enable explicitly wit | Clean apply | Apply config matching on-node, `--skip-post-apply-verify=false` | Silent success (no output, no error) | | Mode=staged | `--mode=staged --skip-post-apply-verify=false` | Phase 2B skipped (staged store doesn't change ActiveID) | | Mode=try | `--mode=try --skip-post-apply-verify=false` | Phase 2B skipped (rollback timer races verify) | -| Mode=reboot | `--mode=reboot --skip-post-apply-verify=false` | Phase 2B skipped (reboot kills the COSI connection mid-verify) | | Mode=auto | `--mode=auto --skip-post-apply-verify=false` | Phase 2B skipped — Talos promotes AUTO to REBOOT internally when the change requires it, so the verify would race the reboot (same shape as the explicit REBOOT skip). Acceptable cost: AUTO applies that don't reboot also lose their verify; pass `--mode=no-reboot` to opt back in | | Mode=no-reboot | Real apply with verify enabled | Phase 2B runs (the only mode where the verify is guaranteed to reach a stable post-apply ActiveID) | | Dry-run | `--dry-run --skip-post-apply-verify=false` | Phase 2B skipped (no real apply) | @@ -1065,7 +1064,7 @@ On by default for `talm upgrade`. The gate fires after talosctl upgrade returns | Same-minor upgrade | `talm upgrade -f node.yaml` to a same-minor image (e.g. v1.12.6 -> v1.12.7) | Silent success; contracts match at minor level | | Cross-minor mismatch | upgrade to `siderolabs/installer:v1.13.0` on a node that rolls back to v1.12 | Hint-bearing blocker citing both versions + two-hypothesis hint (rollback OR slow boot) | | `--skip-post-upgrade-verify` | Pass with any image | Phase 2C suppressed entirely | -| `--insecure` upgrade | Maintenance path — auth-only COSI unreachable | Phase 2C skipped entirely (hard early-return in `shouldRunPostUpgradeVerify(insecure=true, …)`). Distinct from the Phase 2A/2B insecure path, which still calls the COSI reader and degrades gracefully with a "drift verification unavailable on maintenance connection" line — Phase 2C drops the call site itself because there is no graceful degradation path for "verify the version after upgrade" without auth | +| `--insecure` upgrade | Talos v1.14 removed the flag from `upgrade` | `talm upgrade --insecure` fails with `unknown flag: --insecure`. Phase 2C has no maintenance case left to skip | | `--stage` upgrade | New partition not yet activated until reboot — `runtime.Version` would always be the OLD value | Phase 2C skipped via `shouldRunPostUpgradeVerify(staged=true, …)`; guaranteed false positive without skip | | Digest-pinned image | `--image foo/bar@sha256:abc...` | Phase 2C surrenders silently (no tag to parse the target version from) | | Image with no tag | `--image foo/bar` | Phase 2C surrenders silently | @@ -1338,6 +1337,17 @@ talm get metakey --nodes $NODE --endpoints $NODE Expected: table of META keys with their values. +### G1a. `--insecure` does not reach `meta` subcommands on Talos v1.14 + +```bash +talm meta write --insecure 0x0a "test-value" --nodes $NODE --endpoints $NODE +talm meta --insecure write 0x0a "test-value" --nodes $NODE --endpoints $NODE +``` + +Expected on v1.14: both forms fail with `unknown flag: --insecure`. Upstream registers the flag on `metaCmd.Flags()` rather than `PersistentFlags()`, so it reaches neither the subcommand (a local parent flag is not inherited) nor the parent (which takes no args), and `talosctl` behaves the same way. talm mirrors upstream instead of compensating, so META writes against a node in maintenance mode need a Talos-side fix. + +Tracked upstream as [siderolabs/talos#14346](https://github.com/siderolabs/talos/issues/14346). Re-run this case after every Talos bump: once upstream restores the flag, the first form starts working again and this case flips to pinning that. + ### G2. Write a test key ```bash @@ -1366,6 +1376,14 @@ talm bootstrap --nodes $NODE --endpoints $NODE Expected: refuses with `etcd data directory is not empty`. +### H1a. `reset --insecure` is rejected on Talos v1.14 + +```bash +talm reset --insecure --nodes $NODE --endpoints $NODE +``` + +Expected: `unknown flag: --insecure`. Upstream removed the flag from `reset` in v1.14; talm wraps the upstream command and follows it. A node with no usable config is recovered by booting a maintenance image and applying a fresh one. + ### H2. Reset a control-plane node (talm safe default — preserves META) ⚠️ Destructive. Run only against a cluster you can afford to lose one node from. The talm default populates `--system-labels-to-wipe=STATE,EPHEMERAL` automatically when neither `--wipe-mode` nor `--system-labels-to-wipe` was passed, so META survives and the node self-recovers on the next boot. Upstream `talosctl reset` defaults to `--wipe-mode=all`, which destroys META; that path is exposed in talm as the explicit `--wipe-mode=all` opt-in (see H2a). @@ -1488,6 +1506,16 @@ Expected: both invocations exit non-zero with `talm dmesg has been removed` and Regression anchor: a regression that re-enables the upstream `dmesg` wrap (removing it from `excludedCommands` in `pkg/commands/talosctl_wrapper.go`) would either collide with the talm-owned stub at cobra registration, or — if the stub is also dropped — leave operators with the original cryptic `strconv.ParseBool` failure on `--tail=N`. Both shapes are documented elsewhere; this anchor pins the proactive removal + migration-hint contract. +### I0-1b. `template` refuses more than one node + +```bash +talm template -f nodes/node0.yaml --nodes $NODE0,$NODE1 --endpoints $NODE0 +``` + +Expected: `checking node selector: command is not supported with multiple nodes: "talm template"`. A render resolves `lookup` against one node, so the guard rejects the ambiguity rather than picking a node silently. `talm apply` is the command that walks several nodes, one render each. + +The resource walker talm carries in `pkg/engine/talos_helpers.go` (it replaced a talosctl helper dropped in v1.14) is exercised by any single-node render that calls `lookup`, for example the cozystack preset's `lookup "links"` in C1. + ### I0-2. Concurrent dry-run apply ```bash @@ -1541,7 +1569,7 @@ source /tmp/talm-completion.bash Then exercise each target. Each expectation below is a forward-looking check the operator runs interactively (or via `__complete ""` for scripted assertions). - `talm init --preset ` → curated list including `cozystack`. Sourced from `pkg/generated/presets.go::AvailablePresets()`; no generic file completion. -- `talm apply --mode ` → `auto`, `no-reboot`, `reboot`, `staged`, `try` (the apply-mode enum). +- `talm apply --mode ` → exactly the values the flag accepts, read from upstream at completion time. On Talos v1.14 that is `auto`, `no-reboot`, `staged`, `try`; a suggestion the flag then rejects is a bug. - `talm apply --file ` → only `nodes/*.yaml` files that carry a valid `# talm: …` modeline. Non-modelined yaml files in the project tree do NOT surface. Same for `talm template --file ` and `talm upgrade --file `. - `talm template --values ` / `--with-secrets ` → file completion narrowed to `.yaml` / `.yml` extensions (`ShellCompDirectiveFilterFileExt`). - `talm --nodes ` / `talm --endpoints ` → union of nodes / endpoints declared across every context in the active talosconfig. @@ -1680,12 +1708,11 @@ rollback case is real. Pass --skip-post-upgrade-verify to bypass. Phase 2C is **skipped** for the following upgrade flows (each documented in the code): - `--skip-post-upgrade-verify` (operator opt-out) -- `--insecure` (auth-only COSI path is unreachable) - `--stage` (new partition not yet booted; runtime.Version would always report the old version — guaranteed false positive) ### K2-pre. Manual fallback for `--skip-post-upgrade-verify` -K1-pre exercises the automated Phase 2C gate. If the operator disables it (`--skip-post-upgrade-verify`) — or in flows that the gate doesn't cover (`--insecure`, `--stage`, no target image) — the equivalent manual check is: +K1-pre exercises the automated Phase 2C gate. If the operator disables it (`--skip-post-upgrade-verify`) — or in flows that the gate doesn't cover (`--stage`, no target image) — the equivalent manual check is: ```bash target="v1.13.0" @@ -1695,7 +1722,7 @@ running=$(talm get version --nodes $NODE --endpoints $NODE \ test "$running" = "$target" || echo "SILENT ROLLBACK / SLOW BOOT — running $running, expected $target" ``` -This is the post-merge equivalent of what Phase 2C does automatically. Keep the script around — it's still relevant for the `--insecure` flow which the gate skips by design. +This is the post-merge equivalent of what Phase 2C does automatically. Keep the script around — it's still relevant when the gate is disabled or has no target image to compare against. ### K2. Stage-upgrade to a new minor @@ -2056,7 +2083,7 @@ talm template -f nodes/node0.yaml || true ## Sanity-check block -Run after every destructive section (E, F, H, and anything that touches `--mode=reboot` / `--mode=staged` / `apply -I`): +Run after every destructive section (E, F, H, and anything that touches `--mode=auto` on a rebooting change / `--mode=staged` / `apply -I`): ```bash cd $PROJECT @@ -2107,8 +2134,8 @@ talm apply --dry-run -f /tmp/bom.yaml Walk every `--mode` value with `--skip-post-apply-verify=false`: -- `auto`, `no-reboot` → Phase 2B runs. -- `reboot`, `staged`, `try` → Phase 2B auto-skipped (each for a different documented reason). +- `no-reboot` → Phase 2B runs. This is the only mode it runs on. +- `staged`, `try`, `auto` → Phase 2B auto-skipped (each for a different documented reason; `auto` is skipped unconditionally, because Talos promotes it to a reboot whenever the change needs one). - `--dry-run` always skips Phase 2B. ## Cleanup at end of session diff --git a/docs/operations/safety-gates.md b/docs/operations/safety-gates.md index 1529fb68..ebda7394 100644 --- a/docs/operations/safety-gates.md +++ b/docs/operations/safety-gates.md @@ -41,7 +41,7 @@ Reads the node's current MachineConfig via COSI and prints a `+`/`-`/`~`/`=` dif ## 3. Post-apply state verification -After `ApplyConfiguration` returns success, re-reads the on-node MachineConfig and structurally compares it against the bytes that were sent. Divergence blocks the apply chain with a per-document diff, primarily catching silent doc drops (Talos parser ignored an unknown field) and controller reverts. Disabled by default because Talos mutates a handful of leaf fields post-apply (cert hashes, timestamps) that would surface as false-positive divergence without an allowlist. The verify runs only on `--mode=no-reboot`. `--mode=staged`, `--mode=try`, `--mode=reboot`, and `--mode=auto` all skip the gate — each for a documented reason: staged stores rather than activates; try auto-rolls back; reboot kills the COSI connection mid-verify; auto is promoted by Talos to REBOOT internally when the change requires it, so the verify would race the reboot. `--dry-run` skips it too. +After `ApplyConfiguration` returns success, re-reads the on-node MachineConfig and structurally compares it against the bytes that were sent. Divergence blocks the apply chain with a per-document diff, primarily catching silent doc drops (Talos parser ignored an unknown field) and controller reverts. Disabled by default because Talos mutates a handful of leaf fields post-apply (cert hashes, timestamps) that would surface as false-positive divergence without an allowlist. The verify runs only on `--mode=no-reboot`. `--mode=staged`, `--mode=try` and `--mode=auto` all skip the gate — each for a documented reason: staged stores rather than activates; try auto-rolls back; auto is promoted by Talos to REBOOT internally when the change requires it, so the verify would race the reboot that a rebooting apply dispatches, which kills the COSI connection mid-verify. (Talos v1.14 dropped the separate `--mode=reboot` spelling; `--mode=auto` is what reaches that path now.) `--dry-run` skips it too. ## 4. Post-upgrade version verify diff --git a/docs/operations/talosctl-commands.md b/docs/operations/talosctl-commands.md index 574af5d7..fd840ff8 100644 --- a/docs/operations/talosctl-commands.md +++ b/docs/operations/talosctl-commands.md @@ -25,6 +25,26 @@ Every talosctl command is re-exported except these, which talm either replaces o | `upgrade-k8s` | out of scope; talm configures machines, not the Kubernetes control plane | | `dmesg` | retired upstream — use `talm logs kernel --tail=N` | +## `talm meta` — `--insecure` kept reachable + +Talos v1.14 registers `meta`'s `--insecure` on that command's local flag set instead of its persistent one. A local flag on a command that only hosts subcommands reaches nothing: not the subcommands, and not the command itself, which takes no arguments. So `talosctl meta write --insecure` stopped parsing on v1.14, and so did `talosctl meta --insecure write`. + +That is the only way to write a META key over the maintenance service, which is what you do before a node has a machine config, so talm re-publishes such flags as persistent on its wrapper. `talm meta write --insecure` and the `-i` shorthand keep working, and `--cert-fingerprint` travels with them. + +Reported upstream as [siderolabs/talos#14346](https://github.com/siderolabs/talos/issues/14346) and fixed by [siderolabs/talos#14347](https://github.com/siderolabs/talos/pull/14347), which is merged to `main` but not backported to the v1.14 branch. Once the fix ships in a Talos release talm depends on, the wrapper step becomes redundant and is dropped. + +## `talm apply` — `--mode=reboot` is gone on Talos v1.14 + +Upstream stopped registering `reboot` among the values of the apply mode flag in v1.14, so `talm apply --mode=reboot` fails with `invalid argument "reboot" for "-m, --mode" flag`. Use `--mode=auto`, which the node promotes to a reboot when the change requires one. Shell completion reads the accepted values back from the flag, so it no longer suggests `reboot` either. + +## `talm upgrade` — `--insecure` is gone on Talos v1.14 + +Upstream stopped registering `--insecure` on `upgrade` in v1.14 (it had been deprecated before that), so `talm upgrade --insecure` fails with `unknown flag: --insecure`. Upgrading a node that has no valid configuration is done by booting a maintenance image and applying a fresh config. The post-upgrade version verify consequently has no maintenance case left to skip. + +## `talm reset` — `--insecure` is gone on Talos v1.14 + +Upstream dropped `--insecure` from `reset` in v1.14, so `talm reset --insecure` fails with `unknown flag: --insecure`. Resetting a node that has no valid configuration is done from the maintenance side instead: boot the node into a maintenance image and apply a fresh config, rather than resetting over an unauthenticated connection. + ## `talm reset` — META-preserving default `talm reset` diverges from upstream `talosctl reset` on one default. Upstream defaults to `--wipe-mode=all`, which wipes the Talos META partition along with STATE and EPHEMERAL — the node cannot self-recover and comes up in maintenance mode requiring a full re-apply. Talm instead populates `--system-labels-to-wipe=STATE,EPHEMERAL` when neither `--wipe-mode` nor `--system-labels-to-wipe` was passed, which preserves META so the node rejoins the cluster from its META-stored bootstrap config on the next boot. diff --git a/docs/reference/apply.md b/docs/reference/apply.md index 9e557155..01769332 100644 --- a/docs/reference/apply.md +++ b/docs/reference/apply.md @@ -33,28 +33,28 @@ talm apply [flags] ## Options ``` - --cert-fingerprint strings list of server certificate fingeprints to accept (defaults to no check) - --debug show only rendered patches - --dry-run check how the config change will be applied in dry-run mode - -f, --file .yaml node config files / patches (.yaml / `.yml`; shell completion narrows to these extensions). First -f is the modelined anchor (must live under a `talm init`'d project root); subsequent -f files are side-patches stacked onto the anchor's rendered config and may live anywhere. - --force will overwrite existing files - -h, --help help for apply - -i, --insecure apply using the insecure (encrypted with no auth) maintenance service - --kubernetes-version string desired kubernetes version to run (default "1.36.2") - -m, --mode auto, no-reboot, reboot, staged, try apply config mode (default auto) - --set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2). Values that parse as an integer or a boolean are converted to that type; use --set-string to keep them as strings. - --set-file stringArray set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) - --set-json stringArray set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2) - --set-literal stringArray set a literal STRING value on the command line - --set-string stringArray set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2). Nothing is type-converted, so the value reaches the template exactly as typed. - --show-secrets-in-drift show secret-bearing field values verbatim in drift preview / post-apply verify output (default: redacted). Covers both the Talos bootstrap allowlist (cluster.token, cluster.ca.key, machine.token, Wireguard private keys, etc.) and values from encrypted value files (*.encrypted.yaml). Counterpart on template is --show-secrets, which governs the same values in template's stdout render. - --skip-drift-preview skip the pre-apply diff of on-node vs rendered MachineConfig - --skip-post-apply-verify skip the post-apply structural verification of on-node vs sent MachineConfig (default skip until the Talos-mutated field allowlist lands) (default true) - --skip-resource-validation skip the pre-apply check that declared host resources (links, disks) exist on the target node - --talos-version string the desired Talos version to generate config for (backwards compatibility, e.g. v0.8) - --timeout duration the config will be rolled back after specified timeout (if try mode is selected) (default 1m0s) - --values talm template specify values in a YAML file (can specify multiple). Must match talm template — apply re-renders from the modeline and would otherwise drop value files supplied at template time. - --with-secrets string use a secrets file generated using 'gen secrets' + --cert-fingerprint strings list of server certificate fingeprints to accept (defaults to no check) + --debug show only rendered patches + --dry-run check how the config change will be applied in dry-run mode + -f, --file .yaml node config files / patches (.yaml / `.yml`; shell completion narrows to these extensions). First -f is the modelined anchor (must live under a `talm init`'d project root); subsequent -f files are side-patches stacked onto the anchor's rendered config and may live anywhere. + --force will overwrite existing files + -h, --help help for apply + -i, --insecure apply using the insecure (encrypted with no auth) maintenance service + --kubernetes-version string desired kubernetes version to run (default "1.37.0") + -m, --mode auto, no-reboot, staged, try apply config mode (default auto) + --set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2). Values that parse as an integer or a boolean are converted to that type; use --set-string to keep them as strings. + --set-file stringArray set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) + --set-json stringArray set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2) + --set-literal stringArray set a literal STRING value on the command line + --set-string stringArray set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2). Nothing is type-converted, so the value reaches the template exactly as typed. + --show-secrets-in-drift show secret-bearing field values verbatim in drift preview / post-apply verify output (default: redacted). Covers both the Talos bootstrap allowlist (cluster.token, cluster.ca.key, machine.token, Wireguard private keys, etc.) and values from encrypted value files (*.encrypted.yaml). Counterpart on template is --show-secrets, which governs the same values in template's stdout render. + --skip-drift-preview skip the pre-apply diff of on-node vs rendered MachineConfig + --skip-post-apply-verify skip the post-apply structural verification of on-node vs sent MachineConfig (default skip until the Talos-mutated field allowlist lands) (default true) + --skip-resource-validation skip the pre-apply check that declared host resources (links, disks) exist on the target node + --talos-version string the desired Talos version to generate config for (backwards compatibility, e.g. v0.8) + --timeout duration the config will be rolled back after specified timeout (if try mode is selected) (default 1m0s) + --values talm template specify values in a YAML file (can specify multiple). Must match talm template — apply re-renders from the modeline and would otherwise drop value files supplied at template time. + --with-secrets string use a secrets file generated using 'gen secrets' ``` ## Options inherited from parent commands diff --git a/docs/reference/containers.md b/docs/reference/containers.md index fead2d51..cb63a67e 100644 --- a/docs/reference/containers.md +++ b/docs/reference/containers.md @@ -9,9 +9,9 @@ talm containers [flags] ## Options ``` - -f, --file strings specify config files or patches in a YAML file (can specify multiple) - -h, --help help for containers - -k, --kubernetes use the k8s.io containerd namespace + -f, --file strings specify config files or patches in a YAML file (can specify multiple) + -h, --help help for containers + --namespace string namespace to use: "system" (default, Talos service containers), "cri" for Kubernetes workloads, "taloscontainers" for containers declared via ContainerConfig (default "system") ``` ## Options inherited from parent commands diff --git a/docs/reference/debug.md b/docs/reference/debug.md index 9cfd4720..6a2ec208 100644 --- a/docs/reference/debug.md +++ b/docs/reference/debug.md @@ -3,7 +3,7 @@ Run a debug container from an image archive or reference ``` -talm debug [args] [flags] +talm debug [] [flags] ``` ## Examples diff --git a/docs/reference/edit.md b/docs/reference/edit.md index 73a03ff7..54c60d16 100644 --- a/docs/reference/edit.md +++ b/docs/reference/edit.md @@ -18,12 +18,12 @@ talm edit machineconfig [flags] ## Options ``` - --dry-run do not apply the change after editing and print the change summary instead - -f, --file strings specify config files or patches in a YAML file (can specify multiple) - -h, --help help for edit - -m, --mode auto, no-reboot, reboot, staged, try apply config mode (default auto) - --namespace string resource namespace (default is to use default namespace per resource) - --timeout duration the config will be rolled back after specified timeout (if try mode is selected) (default 1m0s) + --dry-run do not apply the change after editing and print the change summary instead + -f, --file strings specify config files or patches in a YAML file (can specify multiple) + -h, --help help for edit + -m, --mode auto, no-reboot, staged, try apply config mode (default auto) + --namespace string resource namespace (default is to use default namespace per resource) + --timeout duration the config will be rolled back after specified timeout (if try mode is selected) (default 1m0s) ``` ## Options inherited from parent commands diff --git a/docs/reference/get.md b/docs/reference/get.md index 9b4bda8f..45d92633 100644 --- a/docs/reference/get.md +++ b/docs/reference/get.md @@ -14,12 +14,13 @@ talm get [] [flags] ## Options ``` - -f, --file strings specify config files or patches in a YAML file (can specify multiple) - -h, --help help for get - -i, --insecure get resources using the insecure (encrypted with no auth) maintenance service - --namespace string resource namespace (default is to use default namespace per resource) - -o, --output string output mode (json, table, yaml, jsonpath) (default "table") - -w, --watch watch resource changes + --cert-fingerprint strings list of server certificate fingerprints to accept (defaults to no check, only used with --insecure flag) + -f, --file strings specify config files or patches in a YAML file (can specify multiple) + -h, --help help for get + -i, --insecure use the insecure (encrypted with no auth) maintenance service + --namespace string resource namespace (default is to use default namespace per resource) + -o, --output string output mode (json, table, yaml, jsonpath) (default "table") + -w, --watch watch resource changes ``` ## Options inherited from parent commands diff --git a/docs/reference/image.md b/docs/reference/image.md index 139eff63..90848033 100644 --- a/docs/reference/image.md +++ b/docs/reference/image.md @@ -7,7 +7,7 @@ Manage container images ``` -f, --file strings specify config files or patches in a YAML file (can specify multiple) -h, --help help for image - --namespace string namespace to use: "system" (etcd and kubelet images), "cri" for all Kubernetes workloads, "inmem" for in-memory containerd instance (default "cri") + --namespace string namespace to use: "system" (etcd and kubelet images), "cri" for all Kubernetes workloads, "inmem" for in-memory containerd instance, "taloscontainers" for containers declared via ContainerConfig (default "cri") ``` ## Options inherited from parent commands diff --git a/docs/reference/image_cache-cert-gen.md b/docs/reference/image_cache-cert-gen.md index d3529ef8..78ae0717 100644 --- a/docs/reference/image_cache-cert-gen.md +++ b/docs/reference/image_cache-cert-gen.md @@ -28,7 +28,7 @@ talm image cache-cert-gen [flags] --cluster string Cluster to connect to if a proxy endpoint is used. --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration - --namespace string namespace to use: "system" (etcd and kubelet images), "cri" for all Kubernetes workloads, "inmem" for in-memory containerd instance (default "cri") + --namespace string namespace to use: "system" (etcd and kubelet images), "cri" for all Kubernetes workloads, "inmem" for in-memory containerd instance, "taloscontainers" for containers declared via ContainerConfig (default "cri") --nodes strings target the specified nodes --root string root directory of the project (default ".") --skip-verify skip TLS certificate verification (keeps client authentication) diff --git a/docs/reference/image_cache-create.md b/docs/reference/image_cache-create.md index cf2925b2..416c4825 100644 --- a/docs/reference/image_cache-create.md +++ b/docs/reference/image_cache-create.md @@ -13,7 +13,7 @@ talm image cache-create [flags] ## Examples ``` -talosctl images cache-create --images=ghcr.io/siderolabs/kubelet:v1.36.2 --image-cache-path=/tmp/talos-image-cache +talosctl images cache-create --images=ghcr.io/siderolabs/kubelet:v1.37.0 --image-cache-path=/tmp/talos-image-cache Alternatively, stdin can be piped to the command: talosctl images default | talosctl images cache-create --image-cache-path=/tmp/talos-image-cache --images=- @@ -41,7 +41,7 @@ talosctl images default | talosctl images cache-create --image-cache-path=/tmp/t --cluster string Cluster to connect to if a proxy endpoint is used. --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration - --namespace string namespace to use: "system" (etcd and kubelet images), "cri" for all Kubernetes workloads, "inmem" for in-memory containerd instance (default "cri") + --namespace string namespace to use: "system" (etcd and kubelet images), "cri" for all Kubernetes workloads, "inmem" for in-memory containerd instance, "taloscontainers" for containers declared via ContainerConfig (default "cri") --nodes strings target the specified nodes --root string root directory of the project (default ".") --skip-verify skip TLS certificate verification (keeps client authentication) diff --git a/docs/reference/image_cache-serve.md b/docs/reference/image_cache-serve.md index 3f1a562e..2d39d110 100644 --- a/docs/reference/image_cache-serve.md +++ b/docs/reference/image_cache-serve.md @@ -28,7 +28,7 @@ talm image cache-serve [flags] --cluster string Cluster to connect to if a proxy endpoint is used. --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration - --namespace string namespace to use: "system" (etcd and kubelet images), "cri" for all Kubernetes workloads, "inmem" for in-memory containerd instance (default "cri") + --namespace string namespace to use: "system" (etcd and kubelet images), "cri" for all Kubernetes workloads, "inmem" for in-memory containerd instance, "taloscontainers" for containers declared via ContainerConfig (default "cri") --nodes strings target the specified nodes --root string root directory of the project (default ".") --skip-verify skip TLS certificate verification (keeps client authentication) diff --git a/docs/reference/image_integration.md b/docs/reference/image_integration.md index 1c729e34..9d19e665 100644 --- a/docs/reference/image_integration.md +++ b/docs/reference/image_integration.md @@ -13,7 +13,7 @@ talm image integration [flags] -h, --help help for integration --installer-tag string tag of the installer image to use --registry-and-user string registry and user to use for the images - --talos-tag string tag of the installer image to use (default "v1.13.7") + --talos-tag string tag of the installer image to use (default "v1.14.0") ``` ## Options inherited from parent commands @@ -22,7 +22,7 @@ talm image integration [flags] --cluster string Cluster to connect to if a proxy endpoint is used. --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration - --namespace string namespace to use: "system" (etcd and kubelet images), "cri" for all Kubernetes workloads, "inmem" for in-memory containerd instance (default "cri") + --namespace string namespace to use: "system" (etcd and kubelet images), "cri" for all Kubernetes workloads, "inmem" for in-memory containerd instance, "taloscontainers" for containers declared via ContainerConfig (default "cri") --nodes strings target the specified nodes --root string root directory of the project (default ".") --skip-verify skip TLS certificate verification (keeps client authentication) diff --git a/docs/reference/image_k8s-bundle.md b/docs/reference/image_k8s-bundle.md index e6fd2ee9..936fa4ed 100644 --- a/docs/reference/image_k8s-bundle.md +++ b/docs/reference/image_k8s-bundle.md @@ -9,13 +9,13 @@ talm image k8s-bundle [flags] ## Options ``` - --coredns-version semver CoreDNS semantic version (default v1.14.4) - --etcd-version semver ETCD semantic version (default v3.6.12) + --coredns-version semver CoreDNS semantic version (default v1.14.7) + --etcd-version semver ETCD semantic version (default 3.7.1) -f, --file strings specify config files or patches in a YAML file (can specify multiple) - --flannel-version semver Flannel CNI semantic version (default 0.28.7) + --flannel-version semver Flannel CNI semantic version (default 0.28.9) -h, --help help for k8s-bundle - --k8s-version semver Kubernetes semantic version (default v1.36.2) - --kube-network-policies-version semver kube-network-policies semantic version (default v1.1.0) + --k8s-version semver Kubernetes semantic version (default v1.37.0) + --kube-network-policies-version semver kube-network-policies semantic version (default v1.1.1) ``` ## Options inherited from parent commands @@ -24,7 +24,7 @@ talm image k8s-bundle [flags] --cluster string Cluster to connect to if a proxy endpoint is used. --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration - --namespace string namespace to use: "system" (etcd and kubelet images), "cri" for all Kubernetes workloads, "inmem" for in-memory containerd instance (default "cri") + --namespace string namespace to use: "system" (etcd and kubelet images), "cri" for all Kubernetes workloads, "inmem" for in-memory containerd instance, "taloscontainers" for containers declared via ContainerConfig (default "cri") --nodes strings target the specified nodes --root string root directory of the project (default ".") --skip-verify skip TLS certificate verification (keeps client authentication) diff --git a/docs/reference/image_list.md b/docs/reference/image_list.md index f1a9f7ae..4dd8437c 100644 --- a/docs/reference/image_list.md +++ b/docs/reference/image_list.md @@ -19,7 +19,7 @@ talm image list [flags] --cluster string Cluster to connect to if a proxy endpoint is used. --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration - --namespace string namespace to use: "system" (etcd and kubelet images), "cri" for all Kubernetes workloads, "inmem" for in-memory containerd instance (default "cri") + --namespace string namespace to use: "system" (etcd and kubelet images), "cri" for all Kubernetes workloads, "inmem" for in-memory containerd instance, "taloscontainers" for containers declared via ContainerConfig (default "cri") --nodes strings target the specified nodes --root string root directory of the project (default ".") --skip-verify skip TLS certificate verification (keeps client authentication) diff --git a/docs/reference/image_pull.md b/docs/reference/image_pull.md index 0b2b323a..e90b83ee 100644 --- a/docs/reference/image_pull.md +++ b/docs/reference/image_pull.md @@ -19,7 +19,7 @@ talm image pull [flags] --cluster string Cluster to connect to if a proxy endpoint is used. --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration - --namespace string namespace to use: "system" (etcd and kubelet images), "cri" for all Kubernetes workloads, "inmem" for in-memory containerd instance (default "cri") + --namespace string namespace to use: "system" (etcd and kubelet images), "cri" for all Kubernetes workloads, "inmem" for in-memory containerd instance, "taloscontainers" for containers declared via ContainerConfig (default "cri") --nodes strings target the specified nodes --root string root directory of the project (default ".") --skip-verify skip TLS certificate verification (keeps client authentication) diff --git a/docs/reference/image_remove.md b/docs/reference/image_remove.md index 28503e8c..5c99a111 100644 --- a/docs/reference/image_remove.md +++ b/docs/reference/image_remove.md @@ -19,7 +19,7 @@ talm image remove [flags] --cluster string Cluster to connect to if a proxy endpoint is used. --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration - --namespace string namespace to use: "system" (etcd and kubelet images), "cri" for all Kubernetes workloads, "inmem" for in-memory containerd instance (default "cri") + --namespace string namespace to use: "system" (etcd and kubelet images), "cri" for all Kubernetes workloads, "inmem" for in-memory containerd instance, "taloscontainers" for containers declared via ContainerConfig (default "cri") --nodes strings target the specified nodes --root string root directory of the project (default ".") --skip-verify skip TLS certificate verification (keeps client authentication) diff --git a/docs/reference/image_talos-bundle.md b/docs/reference/image_talos-bundle.md index 12046ef7..c5bc0f34 100644 --- a/docs/reference/image_talos-bundle.md +++ b/docs/reference/image_talos-bundle.md @@ -21,7 +21,7 @@ talm image talos-bundle [talos-version] [flags] --cluster string Cluster to connect to if a proxy endpoint is used. --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration - --namespace string namespace to use: "system" (etcd and kubelet images), "cri" for all Kubernetes workloads, "inmem" for in-memory containerd instance (default "cri") + --namespace string namespace to use: "system" (etcd and kubelet images), "cri" for all Kubernetes workloads, "inmem" for in-memory containerd instance, "taloscontainers" for containers declared via ContainerConfig (default "cri") --nodes strings target the specified nodes --root string root directory of the project (default ".") --skip-verify skip TLS certificate verification (keeps client authentication) diff --git a/docs/reference/logs.md b/docs/reference/logs.md index 4ad82c13..d8b17546 100644 --- a/docs/reference/logs.md +++ b/docs/reference/logs.md @@ -9,11 +9,13 @@ talm logs [flags] ## Options ``` - -f, --file strings specify config files or patches in a YAML file (can specify multiple) - -F, --follow specify if the logs should be streamed - -h, --help help for logs - -k, --kubernetes use the k8s.io containerd namespace - --tail int32 lines of log file to display (default is to show from the beginning) (default -1) + --cert-fingerprint strings list of server certificate fingerprints to accept (defaults to no check, only used with --insecure flag) + -f, --file strings specify config files or patches in a YAML file (can specify multiple) + -F, --follow specify if the logs should be streamed + -h, --help help for logs + -i, --insecure use the insecure (encrypted with no auth) maintenance service + --namespace string namespace to use: "system" (default, Talos service containers), "cri" for Kubernetes workloads, "taloscontainers" for containers declared via ContainerConfig (default "system") + --tail int32 lines of log file to display (default is to show from the beginning) (default -1) ``` ## Options inherited from parent commands diff --git a/docs/reference/memory.md b/docs/reference/memory.md index 5fae1bbe..3855a676 100644 --- a/docs/reference/memory.md +++ b/docs/reference/memory.md @@ -9,9 +9,11 @@ talm memory [flags] ## Options ``` - -f, --file strings specify config files or patches in a YAML file (can specify multiple) - -h, --help help for memory - -v, --verbose display extended memory statistics + --cert-fingerprint strings list of server certificate fingerprints to accept (defaults to no check, only used with --insecure flag) + -f, --file strings specify config files or patches in a YAML file (can specify multiple) + -h, --help help for memory + -i, --insecure use the insecure (encrypted with no auth) maintenance service + -v, --verbose display extended memory statistics ``` ## Options inherited from parent commands diff --git a/docs/reference/meta.md b/docs/reference/meta.md index 35858a15..1f026b9f 100644 --- a/docs/reference/meta.md +++ b/docs/reference/meta.md @@ -5,9 +5,10 @@ Write and delete keys in the META partition ## Options ``` - -f, --file strings specify config files or patches in a YAML file (can specify multiple) - -h, --help help for meta - -i, --insecure write|delete meta using the insecure (encrypted with no auth) maintenance service + --cert-fingerprint strings list of server certificate fingerprints to accept (defaults to no check, only used with --insecure flag) + -f, --file strings specify config files or patches in a YAML file (can specify multiple) + -h, --help help for meta + -i, --insecure use the insecure (encrypted with no auth) maintenance service ``` ## Options inherited from parent commands diff --git a/docs/reference/meta_delete.md b/docs/reference/meta_delete.md index bf097215..da4eef54 100644 --- a/docs/reference/meta_delete.md +++ b/docs/reference/meta_delete.md @@ -19,7 +19,6 @@ talm meta delete key [flags] --cluster string Cluster to connect to if a proxy endpoint is used. --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration - -i, --insecure write|delete meta using the insecure (encrypted with no auth) maintenance service --nodes strings target the specified nodes --root string root directory of the project (default ".") --skip-verify skip TLS certificate verification (keeps client authentication) diff --git a/docs/reference/meta_write.md b/docs/reference/meta_write.md index ff5504b3..a314e7ae 100644 --- a/docs/reference/meta_write.md +++ b/docs/reference/meta_write.md @@ -19,7 +19,6 @@ talm meta write key value [flags] --cluster string Cluster to connect to if a proxy endpoint is used. --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration - -i, --insecure write|delete meta using the insecure (encrypted with no auth) maintenance service --nodes strings target the specified nodes --root string root directory of the project (default ".") --skip-verify skip TLS certificate verification (keeps client authentication) diff --git a/docs/reference/reset.md b/docs/reference/reset.md index 51c64b47..80d8abbc 100644 --- a/docs/reference/reset.md +++ b/docs/reference/reset.md @@ -13,7 +13,6 @@ talm reset [flags] -f, --file strings specify config files or patches in a YAML file (can specify multiple) --graceful if true, attempt to cordon/drain node and leave etcd (if applicable) (default true) -h, --help help for reset - --insecure reset using the insecure (encrypted with no auth) maintenance service --reboot if true, reboot the node after resetting instead of shutting down --system-labels-to-wipe strings wipe selected system disk partitions by label, keeping others intact (talm default when no wipe flag is set: STATE,EPHEMERAL) --timeout duration time to wait for the operation is complete if --debug or --wait is set (default 30m0s) diff --git a/docs/reference/restart.md b/docs/reference/restart.md index 01dbb3c6..8b3f8bcc 100644 --- a/docs/reference/restart.md +++ b/docs/reference/restart.md @@ -9,9 +9,9 @@ talm restart [flags] ## Options ``` - -f, --file strings specify config files or patches in a YAML file (can specify multiple) - -h, --help help for restart - -k, --kubernetes use the k8s.io containerd namespace + -f, --file strings specify config files or patches in a YAML file (can specify multiple) + -h, --help help for restart + --namespace string namespace to use: "system" (default, Talos service containers), "cri" for Kubernetes workloads, "taloscontainers" for containers declared via ContainerConfig (default "system") ``` ## Options inherited from parent commands diff --git a/docs/reference/service.md b/docs/reference/service.md index 00f6a8ba..ebd16b34 100644 --- a/docs/reference/service.md +++ b/docs/reference/service.md @@ -15,8 +15,10 @@ talm service [ [start|stop|restart|status]] [flags] ## Options ``` - -f, --file strings specify config files or patches in a YAML file (can specify multiple) - -h, --help help for service + --cert-fingerprint strings list of server certificate fingerprints to accept (defaults to no check, only used with --insecure flag) + -f, --file strings specify config files or patches in a YAML file (can specify multiple) + -h, --help help for service + -i, --insecure use the insecure (encrypted with no auth) maintenance service ``` ## Options inherited from parent commands diff --git a/docs/reference/stats.md b/docs/reference/stats.md index ca7f8afd..bfc8b309 100644 --- a/docs/reference/stats.md +++ b/docs/reference/stats.md @@ -9,9 +9,9 @@ talm stats [flags] ## Options ``` - -f, --file strings specify config files or patches in a YAML file (can specify multiple) - -h, --help help for stats - -k, --kubernetes use the k8s.io containerd namespace + -f, --file strings specify config files or patches in a YAML file (can specify multiple) + -h, --help help for stats + --namespace string namespace to use: "system" (default, Talos service containers), "cri" for Kubernetes workloads, "taloscontainers" for containers declared via ContainerConfig (default "system") ``` ## Options inherited from parent commands diff --git a/docs/reference/support.md b/docs/reference/support.md index 4886f669..a16859fc 100644 --- a/docs/reference/support.md +++ b/docs/reference/support.md @@ -23,6 +23,12 @@ Generated bundle contains the following debug information: - Kubernetes nodes and kube-system pods manifests. +By default, the generated bundle is encrypted using age encryption to the list of recipients +set by the members of the 'siderolabs' GitHub organization. The encrypted bundle by default will +only be decryptable by the Sidero Labs team, but you can also specify additional recipients using the +--encryption-recipients flag, or disable encryption completely using the --no-encryption flag. +Default encryption recipients can be removed by setting --encryption-no-default-recipients flag. + ``` talm support [flags] @@ -31,11 +37,16 @@ talm support [flags] ## Options ``` - -f, --file strings specify config files or patches in a YAML file (can specify multiple) - -h, --help help for support - -w, --num-workers int number of workers per node (default 1) - -O, --output string output file to write support archive to - -v, --verbose verbose output + --cert-fingerprint strings list of server certificate fingerprints to accept (defaults to no check, only used with --insecure flag) + --encryption-no-default-recipients do not encrypt to the default recipients, only to the ones provided via --encryption-recipients + --encryption-recipients stringArray additional age recipients (SSH or age public keys) to encrypt the support bundle to (can be specified multiple times) + -f, --file strings specify config files or patches in a YAML file (can specify multiple) + -h, --help help for support + -i, --insecure use the insecure (encrypted with no auth) maintenance service + --no-encryption do not encrypt the support bundle (output is written as-is) + -w, --num-workers int number of workers per node (default 1) + -O, --output string output file to write support archive to + -v, --verbose verbose output ``` ## Options inherited from parent commands diff --git a/docs/reference/template.md b/docs/reference/template.md index f908a824..bd7e5328 100644 --- a/docs/reference/template.md +++ b/docs/reference/template.md @@ -27,7 +27,7 @@ talm template [flags] -h, --help help for template -I, --in-place re-template and update generated files in place (overwrite them) -i, --insecure template using the insecure (encrypted with no auth) maintenance service - --kubernetes-version string desired kubernetes version to run (default "1.36.2") + --kubernetes-version string desired kubernetes version to run (default "1.37.0") --offline disable gathering information and lookup functions --set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2). Values that parse as an integer or a boolean are converted to that type; use --set-string to keep them as strings. --set-file stringArray set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) diff --git a/docs/reference/upgrade.md b/docs/reference/upgrade.md index 97beb6d4..2cb690a2 100644 --- a/docs/reference/upgrade.md +++ b/docs/reference/upgrade.md @@ -45,7 +45,7 @@ talm upgrade [flags] --drain-timeout duration timeout for draining the Kubernetes node (default 5m0s) -f, --file strings specify config files or patches in a YAML file (can specify multiple) -h, --help help for upgrade - -i, --image string the container image to use for performing the install (default "ghcr.io/siderolabs/installer:v1.13.7") + -i, --image string the container image to use for performing the install (default "factory.talos.dev/metal-installer/376567988ad370138ad8b2698212367b8edcb69b5fd68c80be1f2ec7d603b4ba:v1.14.0") --legacy force use of legacy upgrade method --namespace string namespace to use: "system" (etcd and kubelet images), "cri" for all Kubernetes workloads, "inmem" for in-memory containerd instance (default "system") --no-reboot do not reboot the node after upgrade (skip reboot and drain) diff --git a/docs/reference/version.md b/docs/reference/version.md index 5c10dfd2..7b34a08d 100644 --- a/docs/reference/version.md +++ b/docs/reference/version.md @@ -9,11 +9,12 @@ talm version [flags] ## Options ``` - --client Print client version only - -f, --file strings specify config files or patches in a YAML file (can specify multiple) - -h, --help help for version - -i, --insecure use Talos maintenance mode API - --short Print the short version + --cert-fingerprint strings list of server certificate fingerprints to accept (defaults to no check, only used with --insecure flag) + --client Print client version only + -f, --file strings specify config files or patches in a YAML file (can specify multiple) + -h, --help help for version + -i, --insecure use the insecure (encrypted with no auth) maintenance service + --short Print the short version ``` ## Options inherited from parent commands diff --git a/docs/reference/wipe.md b/docs/reference/wipe.md index 59098f9e..fbb374a0 100644 --- a/docs/reference/wipe.md +++ b/docs/reference/wipe.md @@ -27,4 +27,8 @@ Wipe block device or volumes * [talm](index.md) - Manage Talos the GitOps Way! * [talm wipe disk](wipe_disk.md) - Wipe a block device (disk or partition) which is not used as a volume +* [talm wipe lv](wipe_lv.md) - Remove an LVM logical volume +* [talm wipe md](wipe_md.md) - Destroy an MD (software RAID) array +* [talm wipe pv](wipe_pv.md) - Remove an LVM physical volume label +* [talm wipe vg](wipe_vg.md) - Remove an LVM volume group (cascades to its LVs) diff --git a/docs/reference/wipe_disk.md b/docs/reference/wipe_disk.md index 814e8bf4..6b931909 100644 --- a/docs/reference/wipe_disk.md +++ b/docs/reference/wipe_disk.md @@ -15,11 +15,12 @@ talm wipe disk ... [flags] ## Options ``` - --drop-partition drop partition after wipe (if applicable) - -f, --file strings specify config files or patches in a YAML file (can specify multiple) - -h, --help help for disk - -i, --insecure use Talos maintenance mode API - --method string wipe method to use [FAST ZEROES] (default "FAST") + --cert-fingerprint strings list of server certificate fingerprints to accept (defaults to no check, only used with --insecure flag) + --drop-partition drop partition after wipe (if applicable) + -f, --file strings specify config files or patches in a YAML file (can specify multiple) + -h, --help help for disk + -i, --insecure use the insecure (encrypted with no auth) maintenance service + --method string wipe method to use [FAST ZEROES] (default "FAST") ``` ## Options inherited from parent commands diff --git a/docs/reference/wipe_lv.md b/docs/reference/wipe_lv.md new file mode 100644 index 00000000..41161178 --- /dev/null +++ b/docs/reference/wipe_lv.md @@ -0,0 +1,41 @@ +# talm wipe lv + +Remove an LVM logical volume + +## Synopsis + +Remove an LVM logical volume. + +The argument is the qualified logical-volume name, e.g. vg0/lv0. + +``` +talm wipe lv [flags] +``` + +## Options + +``` + --cert-fingerprint strings list of server certificate fingerprints to accept (defaults to no check, only used with --insecure flag) + -f, --file strings specify config files or patches in a YAML file (can specify multiple) + -h, --help help for lv + -i, --insecure use the insecure (encrypted with no auth) maintenance service +``` + +## Options inherited from parent commands + +``` + --cluster string Cluster to connect to if a proxy endpoint is used. + --context string Context to be used in command + -e, --endpoints strings override default endpoints in Talos configuration + --nodes strings target the specified nodes + --root string root directory of the project (default ".") + --skip-verify skip TLS certificate verification (keeps client authentication) + --strict-charts fail if the project's vendored charts/talm/ or pinned preset baseline differs from the talm binary (run talm init --update --preset to re-sync) + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. + --version Print the version number of the application +``` + +## SEE ALSO + +* [talm wipe](wipe.md) - Wipe block device or volumes + diff --git a/docs/reference/wipe_md.md b/docs/reference/wipe_md.md new file mode 100644 index 00000000..323a7111 --- /dev/null +++ b/docs/reference/wipe_md.md @@ -0,0 +1,43 @@ +# talm wipe md + +Destroy an MD (software RAID) array + +## Synopsis + +Stop an MD (software RAID) array and clear the superblock on every member device. + +WARNING: this is destructive. The array must not be in use (mounted or claimed +by another device). The argument is the full array device path, e.g. +/dev/disk/by-id/md-name-data. + +``` +talm wipe md [flags] +``` + +## Options + +``` + --cert-fingerprint strings list of server certificate fingerprints to accept (defaults to no check, only used with --insecure flag) + -f, --file strings specify config files or patches in a YAML file (can specify multiple) + -h, --help help for md + -i, --insecure use the insecure (encrypted with no auth) maintenance service +``` + +## Options inherited from parent commands + +``` + --cluster string Cluster to connect to if a proxy endpoint is used. + --context string Context to be used in command + -e, --endpoints strings override default endpoints in Talos configuration + --nodes strings target the specified nodes + --root string root directory of the project (default ".") + --skip-verify skip TLS certificate verification (keeps client authentication) + --strict-charts fail if the project's vendored charts/talm/ or pinned preset baseline differs from the talm binary (run talm init --update --preset to re-sync) + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. + --version Print the version number of the application +``` + +## SEE ALSO + +* [talm wipe](wipe.md) - Wipe block device or volumes + diff --git a/docs/reference/wipe_pv.md b/docs/reference/wipe_pv.md new file mode 100644 index 00000000..832c575d --- /dev/null +++ b/docs/reference/wipe_pv.md @@ -0,0 +1,43 @@ +# talm wipe pv + +Remove an LVM physical volume label + +## Synopsis + +Wipe LVM metadata from a block device. + +The PV must not be part of an active volume group; remove the VG first with "talosctl wipe vg". + +The argument is a full device path, e.g. /dev/sda1. + +``` +talm wipe pv [flags] +``` + +## Options + +``` + --cert-fingerprint strings list of server certificate fingerprints to accept (defaults to no check, only used with --insecure flag) + -f, --file strings specify config files or patches in a YAML file (can specify multiple) + -h, --help help for pv + -i, --insecure use the insecure (encrypted with no auth) maintenance service +``` + +## Options inherited from parent commands + +``` + --cluster string Cluster to connect to if a proxy endpoint is used. + --context string Context to be used in command + -e, --endpoints strings override default endpoints in Talos configuration + --nodes strings target the specified nodes + --root string root directory of the project (default ".") + --skip-verify skip TLS certificate verification (keeps client authentication) + --strict-charts fail if the project's vendored charts/talm/ or pinned preset baseline differs from the talm binary (run talm init --update --preset to re-sync) + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. + --version Print the version number of the application +``` + +## SEE ALSO + +* [talm wipe](wipe.md) - Wipe block device or volumes + diff --git a/docs/reference/wipe_vg.md b/docs/reference/wipe_vg.md new file mode 100644 index 00000000..00506991 --- /dev/null +++ b/docs/reference/wipe_vg.md @@ -0,0 +1,44 @@ +# talm wipe vg + +Remove an LVM volume group (cascades to its LVs) + +## Synopsis + +Remove an LVM volume group. + +WARNING: this is destructive. Every logical volume inside the group is +removed first, then the volume group itself. There is no separate +confirmation per LV. The underlying physical volumes keep their LVM +labels and remain claimable until you run "talosctl wipe pv". + +``` +talm wipe vg [flags] +``` + +## Options + +``` + --cert-fingerprint strings list of server certificate fingerprints to accept (defaults to no check, only used with --insecure flag) + -f, --file strings specify config files or patches in a YAML file (can specify multiple) + -h, --help help for vg + -i, --insecure use the insecure (encrypted with no auth) maintenance service +``` + +## Options inherited from parent commands + +``` + --cluster string Cluster to connect to if a proxy endpoint is used. + --context string Context to be used in command + -e, --endpoints strings override default endpoints in Talos configuration + --nodes strings target the specified nodes + --root string root directory of the project (default ".") + --skip-verify skip TLS certificate verification (keeps client authentication) + --strict-charts fail if the project's vendored charts/talm/ or pinned preset baseline differs from the talm binary (run talm init --update --preset to re-sync) + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. + --version Print the version number of the application +``` + +## SEE ALSO + +* [talm wipe](wipe.md) - Wipe block device or volumes + diff --git a/go.mod b/go.mod index eb320261..17bed7de 100644 --- a/go.mod +++ b/go.mod @@ -4,107 +4,44 @@ go 1.27.1 // Kubernetes dependencies sharing the same version. require ( - k8s.io/api v0.36.2 - k8s.io/apimachinery v0.36.2 - k8s.io/client-go v0.36.2 - k8s.io/component-base v0.36.2 // indirect -) - -require ( - cloud.google.com/go/compute/metadata v0.9.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.5.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.5.0 // indirect - github.com/aws/aws-sdk-go-v2/config v1.32.30 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30 // indirect - github.com/aws/aws-sdk-go-v2/service/kms v1.54.1 // indirect - github.com/aws/smithy-go v1.27.4 // indirect - github.com/blang/semver/v4 v4.0.0 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/containernetworking/cni v1.3.0 // indirect - github.com/cosi-project/runtime v1.16.2 - github.com/distribution/reference v0.6.0 // indirect - github.com/docker/cli v29.6.2+incompatible // indirect + filippo.io/age v1.3.1 + github.com/BurntSushi/toml v1.6.0 + github.com/Masterminds/sprig/v3 v3.3.0 + github.com/cockroachdb/errors v1.14.0 + github.com/cosi-project/runtime v1.16.3 github.com/dustin/go-humanize v1.0.1 - github.com/fatih/color v1.19.0 // indirect - github.com/foxboron/go-uefi v0.0.0-20251010190908-d29549a44f29 // indirect - github.com/gdamore/tcell/v2 v2.13.10 // indirect - github.com/gertd/go-pluralize v0.2.1 // indirect - github.com/google/cel-go v0.29.2 // indirect - github.com/google/go-containerregistry v0.21.7 // indirect - github.com/google/go-tpm v0.9.8 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/gopacket/gopacket v1.7.0 // indirect + github.com/gobwas/glob v0.2.3 github.com/hashicorp/go-multierror v1.1.1 - github.com/jsimonetti/rtnetlink/v2 v2.2.1-0.20260317095713-310581b9c6ac // indirect - github.com/klauspost/compress v1.19.1 // indirect - github.com/mattn/go-isatty v0.0.23 // indirect - github.com/mdlayher/ethtool v0.6.1 // indirect - github.com/mdlayher/genetlink v1.4.0 // indirect - github.com/mdlayher/netlink v1.11.2 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/opencontainers/runtime-spec v1.3.0 // indirect - github.com/prometheus/procfs v0.21.1 // indirect - github.com/rivo/tview v0.42.0 // indirect - github.com/rs/xid v1.6.0 // indirect - github.com/ryanuber/columnize v2.1.2+incompatible // indirect - github.com/ryanuber/go-glob v1.0.0 // indirect github.com/siderolabs/crypto v0.6.5 - github.com/siderolabs/gen v0.8.7 // indirect - github.com/siderolabs/go-api-signature v0.3.13 // indirect - github.com/siderolabs/go-circular v0.2.3 // indirect - github.com/siderolabs/go-kubeconfig v0.1.2 // indirect - github.com/siderolabs/go-kubernetes v0.2.41 // indirect - github.com/siderolabs/go-pointer v1.0.1 // indirect - github.com/siderolabs/go-procfs v0.1.2 // indirect - github.com/siderolabs/go-retry v0.3.3 // indirect - github.com/siderolabs/go-talos-support v0.2.1 // indirect - github.com/siderolabs/net v0.4.0 // indirect - github.com/siderolabs/proto-codec v0.1.4 // indirect - github.com/siderolabs/talos/pkg/machinery v1.13.7 - github.com/sirupsen/logrus v1.9.4 // indirect + github.com/siderolabs/talos v1.14.0 + github.com/siderolabs/talos/pkg/machinery v1.14.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 - github.com/stretchr/testify v1.11.1 - go.etcd.io/etcd/api/v3 v3.7.0 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.7.0 // indirect - go.etcd.io/etcd/client/v3 v3.7.0 // indirect - go.etcd.io/etcd/etcdutl/v3 v3.7.0 // indirect - go.uber.org/zap v1.28.0 // indirect - golang.org/x/net v0.57.0 // indirect - golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.22.0 // indirect + github.com/stretchr/testify v1.12.1 golang.org/x/sys v0.47.0 golang.org/x/term v0.45.0 - golang.org/x/text v0.40.0 // indirect - golang.org/x/time v0.15.0 // indirect - golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 // indirect - google.golang.org/grpc v1.82.1 - google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af + google.golang.org/grpc v1.83.2 + google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - k8s.io/klog/v2 v2.140.0 // indirect - sigs.k8s.io/hydrophone v0.7.0 // indirect - sigs.k8s.io/yaml v1.6.0 -) - -require ( - filippo.io/age v1.3.1 - github.com/BurntSushi/toml v1.6.0 - github.com/Masterminds/sprig/v3 v3.3.0 - github.com/cockroachdb/errors v1.14.0 - github.com/gobwas/glob v0.2.3 - github.com/siderolabs/talos v1.13.7 helm.sh/helm/v4 v4.2.3 + k8s.io/api v0.37.0 + k8s.io/apimachinery v0.37.0 + k8s.io/client-go v0.37.0 + sigs.k8s.io/yaml v1.6.0 ) require ( c2sp.org/CCTV/age v0.0.0-20260405221107-d5c22d3131c5 // indirect cel.dev/expr v0.25.2 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect dario.cat/mergo v1.0.2 // indirect + filippo.io/edwards25519 v1.2.0 // indirect filippo.io/hpke v0.4.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.5.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.5.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect @@ -116,45 +53,58 @@ require ( github.com/adrg/xdg v0.5.3 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect - github.com/aws/aws-sdk-go-v2 v1.42.1 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.29 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 // indirect - github.com/aws/aws-sdk-go-v2/service/acm v1.42.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.4.1 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.32.1 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.1 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.44.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.43.4 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.35 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.34 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.35 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.35 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.35 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.36 // indirect + github.com/aws/aws-sdk-go-v2/service/acm v1.43.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.35 // indirect + github.com/aws/aws-sdk-go-v2/service/kms v1.55.4 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.5.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.33.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.45.4 // indirect + github.com/aws/smithy-go v1.27.7 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect github.com/brianvoe/gofakeit/v7 v7.14.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.3 // indirect github.com/cloudflare/circl v1.6.4 // indirect github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect github.com/cockroachdb/redact v1.1.8 // indirect - github.com/containerd/containerd/v2 v2.3.3 // indirect + github.com/containerd/containerd/v2 v2.3.4 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/go-cni v1.1.13 // indirect github.com/containerd/log v0.1.0 // indirect + github.com/containernetworking/cni v1.3.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.7.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/creack/pty v1.1.24 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/distribution/reference v0.6.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/docker/cli v29.7.2+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.8 // indirect github.com/emicklei/dot v1.11.0 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect + github.com/fatih/color v1.19.0 // indirect github.com/fluxcd/cli-utils v1.2.2 // indirect github.com/fluxcd/pkg/ssa v0.77.0 // indirect + github.com/foxboron/go-uefi v0.0.0-20251010190908-d29549a44f29 // indirect github.com/fxamacker/cbor/v2 v2.9.2 // indirect github.com/gdamore/encoding v1.0.1 // indirect + github.com/gdamore/tcell/v2 v2.13.10 // indirect + github.com/gertd/go-pluralize v0.2.1 // indirect github.com/getsentry/sentry-go v0.48.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-errors/errors v1.5.1 // indirect @@ -162,34 +112,41 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v1.0.0 // indirect github.com/go-openapi/jsonreference v1.0.0 // indirect - github.com/go-openapi/swag v0.27.3 // indirect - github.com/go-openapi/swag/cmdutils v0.27.3 // indirect - github.com/go-openapi/swag/conv v0.27.3 // indirect - github.com/go-openapi/swag/fileutils v0.27.3 // indirect - github.com/go-openapi/swag/jsonutils v0.27.3 // indirect - github.com/go-openapi/swag/loading v0.27.3 // indirect - github.com/go-openapi/swag/mangling v0.27.3 // indirect - github.com/go-openapi/swag/netutils v0.27.3 // indirect - github.com/go-openapi/swag/pools v0.27.3 // indirect - github.com/go-openapi/swag/stringutils v0.27.3 // indirect - github.com/go-openapi/swag/typeutils v0.27.3 // indirect - github.com/go-openapi/swag/yamlutils v0.27.3 // indirect + github.com/go-openapi/swag v0.28.0 // indirect + github.com/go-openapi/swag/cmdutils v0.28.0 // indirect + github.com/go-openapi/swag/conv v0.28.0 // indirect + github.com/go-openapi/swag/fileutils v0.28.0 // indirect + github.com/go-openapi/swag/jsonutils v0.28.0 // indirect + github.com/go-openapi/swag/loading v0.28.0 // indirect + github.com/go-openapi/swag/mangling v0.28.0 // indirect + github.com/go-openapi/swag/netutils v0.28.0 // indirect + github.com/go-openapi/swag/pools v0.28.0 // indirect + github.com/go-openapi/swag/stringutils v0.28.0 // indirect + github.com/go-openapi/swag/typeutils v0.28.0 // indirect + github.com/go-openapi/swag/yamlutils v0.28.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect + github.com/google/cel-go v0.31.0 // indirect github.com/google/gnostic-models v0.7.1 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-containerregistry v0.21.9 // indirect + github.com/google/go-tpm v0.9.8 // indirect github.com/google/go-tpm-tools v0.4.9-0.20260325175049-22911efba9e5 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gopacket/gopacket v1.7.1 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/gosuri/uilive v0.0.4 // indirect github.com/gosuri/uiprogress v0.0.1 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect + github.com/jsimonetti/rtnetlink/v2 v2.2.1-0.20260802200809-43bafec815b3 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.19.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect @@ -197,6 +154,10 @@ require ( github.com/lmittmann/tint v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/mattn/go-colorable v0.1.15 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect + github.com/mdlayher/ethtool v0.6.1 // indirect + github.com/mdlayher/genetlink v1.4.0 // indirect + github.com/mdlayher/netlink v1.11.2 // indirect github.com/mdlayher/socket v0.6.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect @@ -210,26 +171,44 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/navidys/tvxwidgets v0.14.0 // indirect github.com/neticdk/go-stdlib v1.0.1 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/opencontainers/runtime-spec v1.3.0 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/petermattis/goid v0.0.0-20260716134002-a9b348f0a2b9 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20260702190614-8ae5a48058df // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.24.0 // indirect + github.com/prometheus/client_golang v1.24.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.70.0 // indirect + github.com/prometheus/common v0.70.1 // indirect + github.com/prometheus/procfs v0.21.1 // indirect + github.com/rivo/tview v0.42.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.15.0 // indirect + github.com/rs/xid v1.6.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect github.com/sasha-s/go-deadlock v0.3.9 // indirect github.com/shopspring/decimal v1.4.0 // indirect + github.com/siderolabs/gen v0.8.7 // indirect + github.com/siderolabs/go-api-signature v0.3.13 // indirect + github.com/siderolabs/go-circular v0.2.3 // indirect github.com/siderolabs/go-cmd v0.2.1 // indirect + github.com/siderolabs/go-kubeconfig v0.1.2 // indirect + github.com/siderolabs/go-kubernetes v0.2.41 // indirect + github.com/siderolabs/go-pointer v1.0.1 // indirect + github.com/siderolabs/go-procfs v0.1.2 // indirect + github.com/siderolabs/go-retry v0.3.3 // indirect + github.com/siderolabs/go-talos-support v0.3.1 // indirect + github.com/siderolabs/net v0.4.0 // indirect + github.com/siderolabs/proto-codec v0.1.4 // indirect github.com/siderolabs/protoenc v0.2.4 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect - github.com/stretchr/objx v0.5.3 // indirect github.com/tidwall/gjson v1.19.0 // indirect github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/pretty v1.2.1 // indirect @@ -239,32 +218,46 @@ require ( github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 // indirect github.com/xlab/treeprint v1.2.0 // indirect go.etcd.io/bbolt v1.5.0 // indirect - go.etcd.io/etcd/pkg/v3 v3.7.0 // indirect - go.etcd.io/etcd/server/v3 v3.7.0 // indirect + go.etcd.io/etcd/api/v3 v3.7.1 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.7.1 // indirect + go.etcd.io/etcd/client/v3 v3.7.1 // indirect + go.etcd.io/etcd/etcdutl/v3 v3.7.1 // indirect + go.etcd.io/etcd/pkg/v3 v3.7.1 // indirect + go.etcd.io/etcd/server/v3 v3.7.1 // indirect go.etcd.io/raft/v3 v3.7.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 // indirect - go.opentelemetry.io/otel v1.44.0 // indirect - go.opentelemetry.io/otel/metric v1.44.0 // indirect - go.opentelemetry.io/otel/trace v1.44.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0 // indirect + go.opentelemetry.io/otel v1.45.0 // indirect + go.opentelemetry.io/otel/metric v1.45.0 // indirect + go.opentelemetry.io/otel/trace v1.45.0 // indirect go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect - golang.org/x/crypto v0.54.0 // indirect + golang.org/x/crypto v0.55.0 // indirect golang.org/x/exp v0.0.0-20260718201538-764159d718ef // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a // indirect + golang.org/x/net v0.58.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/text v0.41.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260810153831-ec0a7760b754 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260810153831-ec0a7760b754 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/apiextensions-apiserver v0.36.2 // indirect - k8s.io/cli-runtime v0.36.2 // indirect + k8s.io/apiextensions-apiserver v0.37.0 // indirect + k8s.io/cli-runtime v0.37.0 // indirect + k8s.io/component-base v0.37.0 // indirect + k8s.io/klog/v2 v2.140.0 // indirect k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect - k8s.io/kubectl v0.36.2 // indirect - k8s.io/streaming v0.36.2 // indirect + k8s.io/kubectl v0.37.0 // indirect + k8s.io/streaming v0.37.0 // indirect k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 // indirect sigs.k8s.io/controller-runtime v0.24.1 // indirect + sigs.k8s.io/hydrophone v0.7.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/kustomize/api v0.21.1 // indirect sigs.k8s.io/kustomize/kyaml v0.21.1 // indirect diff --git a/go.sum b/go.sum index f9f14b76..30433360 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/age v1.3.1 h1:hbzdQOJkuaMEpRCLSN1/C5DX74RPcNCk6oqhKMXmZi0= filippo.io/age v1.3.1/go.mod h1:EZorDTYUxt836i3zdori5IJX/v2Lj6kWFU0cfh6C0D4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A= filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= @@ -54,38 +56,38 @@ github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloD github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/aws/aws-sdk-go-v2 v1.42.1 h1:9eOTgu1z/dVtYpNZ3/8/XbbaX0x/BqE3HUzAzs6K0ek= -github.com/aws/aws-sdk-go-v2 v1.42.1/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM= -github.com/aws/aws-sdk-go-v2/config v1.32.30 h1:XwsEzpTJfQYJbFicz/QMLwAZdyeNVVoOEkbF7R3gPJk= -github.com/aws/aws-sdk-go-v2/config v1.32.30/go.mod h1:Ud32SuMc+/9BGxfpSVld7HrE2o05JwKmXY4M3jOQNZU= -github.com/aws/aws-sdk-go-v2/credentials v1.19.29 h1:WHZGssHH887cO0ox07SIQZsFx3MKD4ps6w0xUEmnKYQ= -github.com/aws/aws-sdk-go-v2/credentials v1.19.29/go.mod h1:Mhl0xR6zjguiuj00XRx2wMx22sAltk7oya39sT7fdg8= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30 h1:/hi1JADLEW9YYryEz1w4GQu0EtP23pP553Cf9KgsDV4= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30/go.mod h1:/3AOgy4K17Dm4ucMZVC/MJkzy5kmfKUcINRHZyo0koQ= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 h1:xM/Is9cKMHa8Jj8zkvWhvrFkZsXJV9E+BB4g0HW0duQ= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30/go.mod h1:WueJeNDZvK1fMYEWJIkcivBfEzUkTpBhzlrUKKY8EuA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 h1:jn46zC9LdsVR/ZpMIJqMqb8hHv31BlLx3ulVqNspUOk= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30/go.mod h1:1hTMsAgbdS/AtUi4bw8+gUuh1pceo+eXRLfpSuSQj3M= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 h1:3GUprIsfmGcC5SACIyB0e7E0BM1O1b3Erl5CePYIAeQ= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31/go.mod h1:7PuV1yl5e2xnUbm+RqvVg5i2iBM8EyijZNoI9wsOoOc= -github.com/aws/aws-sdk-go-v2/service/acm v1.42.1 h1:88uIv0OndPiipNtWnCOnm31ee0YLIFYq65hggm4AvZk= -github.com/aws/aws-sdk-go-v2/service/acm v1.42.1/go.mod h1:0HVmvx7Fvvqg+fXgDm66ye6/IF0NKccKS+UkuJyRjpI= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 h1:mbRIur/BiHK6SKPjoBIXSE/hJ6g6JGRLuxQy1jGjlN4= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13/go.mod h1:ITg9em2KbJx1s0y4aqRX5OYWG6HBZ5TVR//OdpEZ2CQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 h1:/Z5jmNrKsSD7EmDjzAPsm/3L9IuOkzaynklJZ1qX7S4= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30/go.mod h1:lEzEZnOosE7zi8Z6royW1cFJTD9fpab4Ul1SBrllewk= -github.com/aws/aws-sdk-go-v2/service/kms v1.54.1 h1:aeJAJyvWS3gQ679pJbz8ZdOh3MViD1zvEdoZMVEawbg= -github.com/aws/aws-sdk-go-v2/service/kms v1.54.1/go.mod h1:0RXNc6Yf3AvSMldGD6Lcch96Ojlw2TtGnHsqfD/L4u8= -github.com/aws/aws-sdk-go-v2/service/signin v1.4.1 h1:V7ZZ300WPXGjvkyore5DGe0ljVPOxCXie/thWdtSBXE= -github.com/aws/aws-sdk-go-v2/service/signin v1.4.1/go.mod h1:mxC0nT/C8wMMS97DemZPzvUZxvIt+2Iq+eS3JdFZGgg= -github.com/aws/aws-sdk-go-v2/service/sso v1.32.1 h1:gYFYh4iLLcAOJRLNPY2aD2g9DIhKn4eof8UkIrr1rTk= -github.com/aws/aws-sdk-go-v2/service/sso v1.32.1/go.mod h1:u8af9Nqkmqnr96f7v9nHqzZT9XBwbXEkTiqT4ROuJSE= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.1 h1:arjT9Cm3/WYbGmD5TUZHk4UQn4Lle1fUNZs5FC6CtF0= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.1/go.mod h1:DMPWJBjYs6+3+f/qhBFEFPPlQ6NlhWjai3dJNvipJ84= -github.com/aws/aws-sdk-go-v2/service/sts v1.44.1 h1:RvfHDg+xvAeZ+5741vUEjpOVtYSIm93W2zhx10Xtydw= -github.com/aws/aws-sdk-go-v2/service/sts v1.44.1/go.mod h1:9gdl4RrflIdpDb2TlXshWgR1F9TeCkvqDx77Vpr4Z/Q= -github.com/aws/smithy-go v1.27.4 h1:JQcphmBN4f0q/sPqXqROIItRNV/hy10cgu7CsFy616M= -github.com/aws/smithy-go v1.27.4/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/aws-sdk-go-v2 v1.43.4 h1:b9FTvbRwy+JCsfp2Wp6wV/KbOx3Aj7nkoFb2cRX0IhE= +github.com/aws/aws-sdk-go-v2 v1.43.4/go.mod h1:70vwSy16txshwG+g55WkpgPKDIByzHI8ccBsOteo3bQ= +github.com/aws/aws-sdk-go-v2/config v1.32.35 h1:UEzXuET8E42lxBPijuACu/tEK7v5lFPlk0Q+GT5WD9E= +github.com/aws/aws-sdk-go-v2/config v1.32.35/go.mod h1:KaMtJpFa2JlL2BStjjHQVwQpzZEmw+ND/EgVrfFoo2g= +github.com/aws/aws-sdk-go-v2/credentials v1.19.34 h1:y6GkSmcv5myd1ngrYbGmiLlwQqB6TQhOuN/tbSSuWDY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.34/go.mod h1:w3dTcnDVoQIewjo7JG45hduAToikiIFLC4FIO7fndvw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.35 h1:+S7kbJoLDDQ5tE+lHrUBgMkzC8NLgsaioS2F3dVoFAE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.35/go.mod h1:Ak7xXviIARfFdNUJ9Etb0bdVDt/KAvKjMGJVLWXDzik= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.35 h1:kzVuGlatQtYinwBJEEyLAbggepCoavosiaHHX9+fD+c= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.35/go.mod h1:0yLx0yEI+SfqeJMPvOtIEFoZbiQYXMGszBueiutQyaI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.35 h1:WK6CjihTuLisCjSKKbildJ79sGZZgbBz3iNa7VsKIhU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.35/go.mod h1:KYleN57luLoe97R7vTnx8PMcVrr9gAcRECtOjl91DNg= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.36 h1:jbGY4CXLzZElOXgGsexlC3Hi+3YM0rSmk4opFXKqg/k= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.36/go.mod h1:uBu/9aKsS/UQGc72RAt3y54kjgYQxmhut8ZD2dXCDNE= +github.com/aws/aws-sdk-go-v2/service/acm v1.43.4 h1:Vq/B0ruqtv6bNAatkx7i9nhaX8aTLz+g0mx6ZuBKqfE= +github.com/aws/aws-sdk-go-v2/service/acm v1.43.4/go.mod h1:o6neSZchmZ2Bqxy1BD4DkdftKVskqNNIRViAZquAbQA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.15 h1:JJLBQxwY+AFwuPAi5ivGc1ChnTdUt4cXMv7e76m2c/Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.15/go.mod h1:lQknBIe78MVL0cQOQDlag8KGflMbMEVFx9mB6O8ENvk= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.35 h1:BBEElKh4a+rKshvjrfpajTe9CbpZvrbb4Jkg2PB7RzA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.35/go.mod h1:zaZk983w//8beSruBVec/mr4CmDwgZitW/qzGhAAX0g= +github.com/aws/aws-sdk-go-v2/service/kms v1.55.4 h1:8T9CDPlcIUpXTKXXfMMFtD1eujGXbVysGiidx79bTkc= +github.com/aws/aws-sdk-go-v2/service/kms v1.55.4/go.mod h1:XlYycjMbh9zYnTPpjUropzSDngZd/x37jNa9vGHA7hE= +github.com/aws/aws-sdk-go-v2/service/signin v1.5.4 h1:cOJELVNrq5Q3Udry2GLuHUM7MhwpeaQRdYaoa6GI/yI= +github.com/aws/aws-sdk-go-v2/service/signin v1.5.4/go.mod h1:f4LxzKBtaTxD7xh3PiVg3CE1tchQemfmghaJr+NbK2c= +github.com/aws/aws-sdk-go-v2/service/sso v1.33.4 h1:AMW7a7S8iQaHjBYZdU3PCq4GKRPijTPRAc7e6XtEThY= +github.com/aws/aws-sdk-go-v2/service/sso v1.33.4/go.mod h1:QQNsFV1DVXoXcZt18FS8lI8rtUrlDyAuWZLQ5shunv4= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.4 h1:AsbZcJAQPRmHDJG8K1N0pof/1zPWjVT8TFlTWuGLSvo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.4/go.mod h1:6imqztH0//t0mKbl6yWl7swSEl7F/w32oAmqB3vP1ag= +github.com/aws/aws-sdk-go-v2/service/sts v1.45.4 h1:w/AryDYMjSUANSQ2uoZxJovUsMTwWJNTv3IMex30Y+4= +github.com/aws/aws-sdk-go-v2/service/sts v1.45.4/go.mod h1:WeBiAa67azG7Su9Vf+ChGDBLiAozJCXzdjXiPBUwtbc= +github.com/aws/smithy-go v1.27.7 h1:Zgj5z4LfcDYoQIVk+n/yGdTkP/2y6ZT5vYxe0fp7bqE= +github.com/aws/smithy-go v1.27.7/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -100,8 +102,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80= github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= -github.com/cilium/ebpf v0.21.0 h1:4dpx1J/B/1apeTmWBH5BkVLayHTkFrMovVPnHEk+l3k= -github.com/cilium/ebpf v0.21.0/go.mod h1:1kHKv6Kvh5a6TePP5vvvoMa1bclRyzUXELSs272fmIQ= +github.com/cilium/ebpf v0.22.0 h1:v2ktp0roffpMOj2MMf3idtCQZOsAoC4BJbAJN+ke2bY= +github.com/cilium/ebpf v0.22.0/go.mod h1:CDzZbe2hC5JjlDC+CY3KFCzlYwN4gbxppYM+Z10bQt4= github.com/cloudflare/circl v1.6.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U= github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY= github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= @@ -112,8 +114,8 @@ github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILM github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506/go.mod h1:Mw7HqKr2kdtu6aYGn3tPmAftiP3QPX63LdK/zcariIo= github.com/cockroachdb/redact v1.1.8 h1:8eVLLj6juKxiKrAEw2b8cJvNqWq++U8WOfQFuL7KTaA= github.com/cockroachdb/redact v1.1.8/go.mod h1:GceHHpJ0rMDpYARL5In88Alq/xMBUtVlz7Qxix6ZVkw= -github.com/containerd/containerd/v2 v2.3.3 h1:MUNBVVBTBpPll7KPh5GTvkC3cfG03PQLAHVdsUoue9k= -github.com/containerd/containerd/v2 v2.3.3/go.mod h1:rHKGm3VW6wNrINb3x8mNT+w7qYXFVElTt/8HTuxVhD4= +github.com/containerd/containerd/v2 v2.3.4 h1:c2PJo/9UGVdiiw8SwrxuLxWGY+9b3jQ6Xp9zntneIvI= +github.com/containerd/containerd/v2 v2.3.4/go.mod h1:a30D8fWZJ1Uzx/2WpjLbLsxBkq9He41pe8ENW+QZ3LY= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/go-cni v1.1.13 h1:eFSGOKlhoYNxpJ51KRIMHZNlg5UgocXEIEBGkY7Hnis= @@ -126,8 +128,8 @@ github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= -github.com/cosi-project/runtime v1.16.2 h1:sgjOMrnaElrja93kkJJJIQYwd45PhpGl9cBci9yqn5c= -github.com/cosi-project/runtime v1.16.2/go.mod h1:+GrSnmJjMfWMe6NubevwwXQf/v7afddDLeCbLonvvps= +github.com/cosi-project/runtime v1.16.3 h1:EQ1oubPjAVPnAFc5GhSGfHuiPcm8b2g2XsPnpl+Wq0g= +github.com/cosi-project/runtime v1.16.3/go.mod h1:p3nyBuIqeipcfQHuM9uGjLkFN2gYA9GJFL4oSEEtmm0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -142,8 +144,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/cli v29.6.2+incompatible h1:/bjePvcbbFTnRrMfWJBY7AjfICdsiLVgHn6LwTVOcqw= -github.com/docker/cli v29.6.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.7.2+incompatible h1:dlkwallR8XqfeVnA2ELEhdwvb4lsSwuB4IgsG8Q9cLY= +github.com/docker/cli v29.7.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.9.8 h1:bIREROb7So6PRlq6KTtdS9MPEjC29OQRkFNlvK2OX8Q= github.com/docker/docker-credential-helpers v0.9.8/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -193,32 +195,32 @@ github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxg github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY= github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0= -github.com/go-openapi/swag v0.27.3 h1:i6oVKkGZeFgETHMiBHGtj9gIQ1aLtWDdJnT/SRZeets= -github.com/go-openapi/swag v0.27.3/go.mod h1:qEXs3GcyyQTDCFQ4ykqnLPDh8qT+zBcjbVWdxCAW0Us= -github.com/go-openapi/swag/cmdutils v0.27.3 h1:sjuL0TvW81i9R9GRMO/fy+c3mOW+7zxRYwy/7fobZt4= -github.com/go-openapi/swag/cmdutils v0.27.3/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= -github.com/go-openapi/swag/conv v0.27.3 h1:iqJFmGEjmX3AY0lSszABFqRVqOSt99XS0LzNIMJYuhU= -github.com/go-openapi/swag/conv v0.27.3/go.mod h1:nPRmN6jgNme99hpf+nM0auDZGALWIqlwhisKPK/bQhQ= -github.com/go-openapi/swag/fileutils v0.27.3 h1:3UVoZ2RLaIs1lt+2jcKzL8RM3Yk0rmsDE9FLA/HGxFE= -github.com/go-openapi/swag/fileutils v0.27.3/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8= -github.com/go-openapi/swag/jsonutils v0.27.3 h1:1DEz+O82frtSMBcos/7XIn1GnpNTbsD4Bru4Dc/uhRc= -github.com/go-openapi/swag/jsonutils v0.27.3/go.mod h1:qiDCoQvzkMxrV3G8FLEdIU5L+EFYc0zcDOHWT3Yofvo= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3 h1:h/eT9kmGCDdFLJF29lOhzLtF0FmP1AX2MhLJWVebsb8= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY= -github.com/go-openapi/swag/loading v0.27.3 h1:L9nQkEgzU7QgFQL+pLEMfGUKxeM4pWwGwbET9Z3weW0= -github.com/go-openapi/swag/loading v0.27.3/go.mod h1:rJ0NeaKsF4CVPnMGjPQl7JlSHzvD0bc2DKXLss1hiuE= -github.com/go-openapi/swag/mangling v0.27.3 h1:gRzzD1PAUoLTtGMgI3KpBmCSOlTuLTFWnviLxLcTnyg= -github.com/go-openapi/swag/mangling v0.27.3/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w= -github.com/go-openapi/swag/netutils v0.27.3 h1:IoBvfCoprsE6E87kAIm9basnISqDDqB79mJ8MN+f5PU= -github.com/go-openapi/swag/netutils v0.27.3/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ= -github.com/go-openapi/swag/pools v0.27.3 h1:gXjImP3F6/56wRRcFgEPld084Y6u2gs21ikPBt8NKBk= -github.com/go-openapi/swag/pools v0.27.3/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE= -github.com/go-openapi/swag/stringutils v0.27.3 h1:Ru28hnbAvN5wycALQYy8IobHvASq+FUFMlp1QzLM0JI= -github.com/go-openapi/swag/stringutils v0.27.3/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM= -github.com/go-openapi/swag/typeutils v0.27.3 h1:l6SSrx5eR5/WVwrGNzN6bQ9WqL04mrxNBl9YgQ3rcJ4= -github.com/go-openapi/swag/typeutils v0.27.3/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= -github.com/go-openapi/swag/yamlutils v0.27.3 h1:cRFCAoYtslYn9L9T0xWryHy1t7c1MACC+DMj3CLvwvs= -github.com/go-openapi/swag/yamlutils v0.27.3/go.mod h1:6JYBGj8sw/NawMllyZY+cTA8Mzk2etS3ZBASdcyPsiU= +github.com/go-openapi/swag v0.28.0 h1:xkgbOSKj6DZziNpyqRRAOt3GJGtgjgsd2RoyT30VWuw= +github.com/go-openapi/swag v0.28.0/go.mod h1:4qYnT3Cqr1p1VknOdPo70evN4rgQnAg6jwApHyxSGIg= +github.com/go-openapi/swag/cmdutils v0.28.0 h1:7TOeNtkYru1SG8Y34tDh9WBbLsMqGnptuxWiHREPZ4Q= +github.com/go-openapi/swag/cmdutils v0.28.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= +github.com/go-openapi/swag/conv v0.28.0 h1:GtqqbyFe7vR5Y7ehxG9W6/OvrSFdf1OLeTGp40TqxH8= +github.com/go-openapi/swag/conv v0.28.0/go.mod h1:mbUE+mzctnhxi864m0Q07SpN8OowD9JhxmxuYvZZD/k= +github.com/go-openapi/swag/fileutils v0.28.0 h1:Z04XWQD7R8Eq+7GnOrjovBxPPmZzsS4gt2H2GPGIViU= +github.com/go-openapi/swag/fileutils v0.28.0/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8= +github.com/go-openapi/swag/jsonutils v0.28.0 h1:YIch6FwO7RXzeAnbO8Tu7dWBZeUEH+4nA0HXltVTnv4= +github.com/go-openapi/swag/jsonutils v0.28.0/go.mod h1:CYM3WlTUcagR2ZoHdz54di/cbBqt82tuxuXgAjxw+mg= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.28.0 h1:qV+VVUAx5Oro8WjVWpZeql7YReTKhT4smR4zhcOQZr0= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.28.0/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY= +github.com/go-openapi/swag/loading v0.28.0 h1:td8QZdZC9MIYGGSnSPKShKiK22I2tU5UQvuUhIBPRLU= +github.com/go-openapi/swag/loading v0.28.0/go.mod h1:rXB0QiQX5mMveXEA7ouM4KiiM9jVJe4K6BVbwhD1M4k= +github.com/go-openapi/swag/mangling v0.28.0 h1:pH8eyeNO9SLYsTMWJrurnNfKmDa28XrlA+HePVD53VM= +github.com/go-openapi/swag/mangling v0.28.0/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w= +github.com/go-openapi/swag/netutils v0.28.0 h1:YXN6TALEi2pzts8/8GNm6T61HTAZsieukGZidap989k= +github.com/go-openapi/swag/netutils v0.28.0/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ= +github.com/go-openapi/swag/pools v0.28.0 h1:HPMZWSAfce3rdVTFcjFiCIBtDg9h4x2QlRrHipwhxeU= +github.com/go-openapi/swag/pools v0.28.0/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE= +github.com/go-openapi/swag/stringutils v0.28.0 h1:ixsc9iYgDPubHL/8nSkbnryEHpD2VRlBMLKpQyPXcDU= +github.com/go-openapi/swag/stringutils v0.28.0/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM= +github.com/go-openapi/swag/typeutils v0.28.0 h1:nRBKSBXjDgf01VDPB3fWeD9nQuhCOVeIYAkUx2tbkyY= +github.com/go-openapi/swag/typeutils v0.28.0/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= +github.com/go-openapi/swag/yamlutils v0.28.0 h1:TV3JXH6DS46KUroDtMLAYHGkdWf5VDq3wVWFirmzROY= +github.com/go-openapi/swag/yamlutils v0.28.0/go.mod h1:x0q/yndZHEgk9Rx3DyDqzFUmHy55KTvIZldvF2dTJXs= github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0= github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo= github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug= @@ -235,14 +237,14 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.29.2 h1:ZtDxkeiMmz0mxbKDYiNkE5Lk7V5edMRcaaDf2jX002k= -github.com/google/cel-go v0.29.2/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/cel-go v0.31.0 h1:H0bhpFTqOvmHrBGrWKp7ZlhBm5Hh8PYUEXnwxT1LL7A= +github.com/google/cel-go v0.31.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-containerregistry v0.21.7 h1:/vPFuVXDjtFREsVArW+0h1CIl5urnOhzei4X2DMW9IU= -github.com/google/go-containerregistry v0.21.7/go.mod h1:kjSbt7/zMsKLWfnHrIvKvhXHUw91jbe9DNjPPJ32gXE= +github.com/google/go-containerregistry v0.21.9 h1:F+D4uZ3iA3DLMJLfhaqMdHJbzeqm/216WGQq2dokuLs= +github.com/google/go-containerregistry v0.21.9/go.mod h1:dP5XNKcL7kMFF/TB3LfvWmVhAcv7iqkHb3oDK8aauTo= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/go-tpm-tools v0.4.9-0.20260325175049-22911efba9e5 h1:WKRLDs3+G0hJjP+3EdjQwUpqTVZ0E33qZs1lTiN1I1o= @@ -252,8 +254,8 @@ github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oX github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gopacket/gopacket v1.7.0 h1:GdmF8ytGnjtSvyy30CTZhIwX1ybDWH3Q0MNK0blIKzA= -github.com/gopacket/gopacket v1.7.0/go.mod h1:QKowPlTLrQU2rqV5C5I14Aoaid3l8da3kbddibc/Wgk= +github.com/gopacket/gopacket v1.7.1 h1:1C7/wrJ5HyiEAYDtStJHQk4rV0ChpanZDV9+3Ov3gaM= +github.com/gopacket/gopacket v1.7.1/go.mod h1:QKowPlTLrQU2rqV5C5I14Aoaid3l8da3kbddibc/Wgk= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY= @@ -264,8 +266,8 @@ github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -277,16 +279,16 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= -github.com/jsimonetti/rtnetlink/v2 v2.2.1-0.20260317095713-310581b9c6ac h1:UfziP9RaDM6D+f+yNdL3T/N1DztwltLDGBkpybF/fYs= -github.com/jsimonetti/rtnetlink/v2 v2.2.1-0.20260317095713-310581b9c6ac/go.mod h1:A/gqt1BEMJcvzGQJXQ3SnsDOQL7QRNhxTiC3eb++608= +github.com/jsimonetti/rtnetlink/v2 v2.2.1-0.20260802200809-43bafec815b3 h1:J6v2VXhjrJoRebtENwDSEphEuVK37akK2xS5qFnGqyc= +github.com/jsimonetti/rtnetlink/v2 v2.2.1-0.20260802200809-43bafec815b3/go.mod h1:kJVDnzZlnEw9spAMlgqheZICuD+kX3S7/dcBjNRvnAQ= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= -github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8= +github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -301,8 +303,8 @@ github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyiQ= -github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mdlayher/ethtool v0.6.1 h1:fSfcX6EN3yBqcB+vsCnq8hpbIT4vEa7T+BKb0NjT894= github.com/mdlayher/ethtool v0.6.1/go.mod h1:ezmdXM273WHGEt1OvaeqvKAkcjw28/dbYYeQuC2aIxQ= github.com/mdlayher/genetlink v1.4.0 h1:f/Xs7Y2T+GyX9b3dbiUhnLE9InGs5F9RxJ2JwBMl71o= @@ -337,8 +339,8 @@ github.com/navidys/tvxwidgets v0.14.0 h1:8UDKTDbbQeWwmOBcsTzKTrUYv7BpGYphpib7zWt github.com/navidys/tvxwidgets v0.14.0/go.mod h1:yILrJRJqf399gw7JyUM7UdT+e9PMMDXpk+CuIoqvEj8= github.com/neticdk/go-stdlib v1.0.1 h1:3P6tJIICo8kvMMEFWSZCk+iRh+HoN8P/51WwMO+Ka2k= github.com/neticdk/go-stdlib v1.0.1/go.mod h1:KP9nLuDoanLbM8Wturn+hage2FtcrJaF1+1Znu+MKEw= -github.com/onsi/ginkgo/v2 v2.30.0 h1:zxM/9XneXFIy64j6/wAmBIX4zRC7Hu6U8XFNZvDnCQc= -github.com/onsi/ginkgo/v2 v2.30.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= +github.com/onsi/ginkgo/v2 v2.32.0 h1:Hw7s2pVrQo/8Yz5N77qdnpHaoc+c6cC9WIV1Jce+J6E= +github.com/onsi/ginkgo/v2 v2.32.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I= github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -359,17 +361,17 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjL github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25 h1:S1hI5JiKP7883xBzZAr1ydcxrKNSVNm7+3+JwjxZEsg= -github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25/go.mod h1:ZQntvDG8TkPgljxtA0R9frDoND4QORU1VXz015N5Ks4= +github.com/planetscale/vtprotobuf v0.6.1-0.20260702190614-8ae5a48058df h1:x2ymdov8jnZLDPfI+VVcf/ZvzuZ2u36ieXTuQASMkWI= +github.com/planetscale/vtprotobuf v0.6.1-0.20260702190614-8ae5a48058df/go.mod h1:araspv2uYKozbi5lrKaqpv1/Uei7eQSml8JCw2A3IRg= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.24.0 h1:5XStIklKuAtJSNpdD3s8XJj/Yv78IQmE1kbNk87JrAI= -github.com/prometheus/client_golang v1.24.0/go.mod h1:QcsNdotprC2nS4BTM2ucbcqxd2CeXTEa9jW7zHO9iDE= +github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= +github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.70.0 h1:bcpru3tWPVnxGnETLgOV5jbp/JRXgYEyv65CuBLAMMI= -github.com/prometheus/common v0.70.0/go.mod h1:S/SFasQmgGiYH6C81LKCtYa8QACgthGg5zxL2udV7SY= +github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY= +github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= github.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c= @@ -383,12 +385,10 @@ github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF25UfsK2X7Ph/zCAk= -github.com/ryanuber/columnize v2.1.2+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sasha-s/go-deadlock v0.3.9 h1:fiaT9rB7g5sr5ddNZvlwheclN9IP86eFW9WgqlEQV+w= github.com/sasha-s/go-deadlock v0.3.9/go.mod h1:KuZj51ZFmx42q/mPaYbRk0P1xcwe697zsJKE03vD4/Y= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= @@ -415,18 +415,18 @@ github.com/siderolabs/go-procfs v0.1.2 h1:bDs9hHyYGE2HO1frpmUsD60yg80VIEDrx31fkb github.com/siderolabs/go-procfs v0.1.2/go.mod h1:dBzQXobsM7+TWRRI3DS9X7vAuj8Nkfgu3Z/U9iY3ZTY= github.com/siderolabs/go-retry v0.3.3 h1:zKV+S1vumtO72E6sYsLlmIdV/G/GcYSBLiEx/c9oCEg= github.com/siderolabs/go-retry v0.3.3/go.mod h1:Ff/VGc7v7un4uQg3DybgrmOWHEmJ8BzZds/XNn/BqMI= -github.com/siderolabs/go-talos-support v0.2.1 h1:QVFhcmGw1ZTTXEtukBgrdjNxYbsPAOTMpopisV4EbgU= -github.com/siderolabs/go-talos-support v0.2.1/go.mod h1:tfwP9mpPdmqLU8DuCDgcAd0ficLlJuB/XMtrIV8g+20= +github.com/siderolabs/go-talos-support v0.3.1 h1:2cIct5nxGby8B7NNALO5WL3WbCe9S1se/3/nT0H/x7Q= +github.com/siderolabs/go-talos-support v0.3.1/go.mod h1:3fljgD3YQGB2+nJfWLLt482/C8epdqge70fWH+VtuiU= github.com/siderolabs/net v0.4.0 h1:1bOgVay/ijPkJz4qct98nHsiB/ysLQU0KLoBC4qLm7I= github.com/siderolabs/net v0.4.0/go.mod h1:/ibG+Hm9HU27agp5r9Q3eZicEfjquzNzQNux5uEk0kM= github.com/siderolabs/proto-codec v0.1.4 h1:AcrLot/251qAa35GYhSqDeEKF3bygOlNaYJSrl7MPNQ= github.com/siderolabs/proto-codec v0.1.4/go.mod h1:frX2VEBSrZwzGxEdT+AMSjiWr6ySY/rCSXJxD0z9kxQ= github.com/siderolabs/protoenc v0.2.4 h1:D3Fpn2nQSQOhl8ZlAxijZAf7K6F8CM1uZq0afIGsr8Q= github.com/siderolabs/protoenc v0.2.4/go.mod h1:i5XLHjfv5vyi7LhQrSEo19HCA+lYtDd7CWxsoWp9XE8= -github.com/siderolabs/talos v1.13.7 h1:lxIkUajbneQMM/lIVS+JTOn1TwSLCgYwe4E4/M72FNM= -github.com/siderolabs/talos v1.13.7/go.mod h1:JLVfZ8e1wJ9lHcZtBwzljNcrw686505rdn0/UeCfE6g= -github.com/siderolabs/talos/pkg/machinery v1.13.7 h1:0cPB37GT83zDKYueh2saaet3wXy00mUdkQcdPqrbK60= -github.com/siderolabs/talos/pkg/machinery v1.13.7/go.mod h1:2PtzQFjql5bg9YNp8gv1vARJT5rhmBUtoj1+fYXbLk8= +github.com/siderolabs/talos v1.14.0 h1:A1RIiEj4jObjRsZB5HC/KDmKokhU4pXsvixD+57czKw= +github.com/siderolabs/talos v1.14.0/go.mod h1:1QtvU+kWmeTs36e/Qf1NzAyQryD1d4tXLJLJkg1ANnM= +github.com/siderolabs/talos/pkg/machinery v1.14.0 h1:wVaf0IZhJLaOsr8OHvYR8RTDN4Gh9qqStd904eM5RlE= +github.com/siderolabs/talos/pkg/machinery v1.14.0/go.mod h1:8rhGLm+Se7X/VnTG6InEhagg7zCDUFRwfJjLcqGNw7w= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= @@ -446,8 +446,8 @@ github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+Q github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= @@ -478,38 +478,38 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU= go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk= -go.etcd.io/etcd/api/v3 v3.7.0 h1:WZlGK7pRtYGDB8ti8wkrQ5D2oWGMbtNL9VA5e+vF2Fg= -go.etcd.io/etcd/api/v3 v3.7.0/go.mod h1:EcTihnwAQ0BQNh5dfAdaFVFdchuo7EP0HlX7TV3jz/A= -go.etcd.io/etcd/client/pkg/v3 v3.7.0 h1:sW9njJzS3vXKcAJjjLQ4nk+avNUJ12Bcijcx8ehUskE= -go.etcd.io/etcd/client/pkg/v3 v3.7.0/go.mod h1:cnzZGIUzSfjEwLC6UBVsSXlEK1eepS/JUD7wE6PLRT0= -go.etcd.io/etcd/client/v3 v3.7.0 h1:5MHO37VbPB87VRPKUXEcicjeQWiTSjpPv3Ume8xPx20= -go.etcd.io/etcd/client/v3 v3.7.0/go.mod h1:DJ382WuwjmbowjPDyaaQ0idWXy4dh91XRhe4FOrb9vM= -go.etcd.io/etcd/etcdutl/v3 v3.7.0 h1:NWpz72HizSHVmOK+zDA6LwLpFBq5qAQiiJm3VV55rDY= -go.etcd.io/etcd/etcdutl/v3 v3.7.0/go.mod h1:4l1DPx/nyxKAC4unO7cfDQwC1Gxleb+2AjCD25gZDt4= -go.etcd.io/etcd/pkg/v3 v3.7.0 h1:aUBHGzXnYL2m3AlZfKp0ZhHQLVZIKfu9QsIlYdr+mxc= -go.etcd.io/etcd/pkg/v3 v3.7.0/go.mod h1:fDQYyc8rOC1Yl4EZLh0O1OjHzwJW7jAly80+BpD1M9o= -go.etcd.io/etcd/server/v3 v3.7.0 h1:ScdUdN8ljuimp0lZaNq0otLMrHcFSFT+dQyT1j7JSFo= -go.etcd.io/etcd/server/v3 v3.7.0/go.mod h1:v7N1dPdSW2vzyxGsbDwK4X0sCe3SYNlQ+9eNIMGzdxQ= +go.etcd.io/etcd/api/v3 v3.7.1 h1:KJG0/DcWGfe3Y1otDf/fsBf0TSSgpxZ5RO/L8SFt73E= +go.etcd.io/etcd/api/v3 v3.7.1/go.mod h1:8bXIpCMeV7E3/XL0Ix123ATn3dB+0V7d9zklHbB0m78= +go.etcd.io/etcd/client/pkg/v3 v3.7.1 h1:rKYsj3pRkR0eK3yjT3XOgrhqfmIfj9pzNgxjh7mfFv4= +go.etcd.io/etcd/client/pkg/v3 v3.7.1/go.mod h1:cnzZGIUzSfjEwLC6UBVsSXlEK1eepS/JUD7wE6PLRT0= +go.etcd.io/etcd/client/v3 v3.7.1 h1:0PEMMC0KuZmVIN+RAbdqfkZ45pYTgKVtmBEbRCvZFUg= +go.etcd.io/etcd/client/v3 v3.7.1/go.mod h1:ffNqALa8tRCYhYo1F9oR489y23K39Gz+BSR3ApAGYq0= +go.etcd.io/etcd/etcdutl/v3 v3.7.1 h1:CTZEqFhNkhq/VmaednQpItaORwYsgPDEuOuHs0vZCK4= +go.etcd.io/etcd/etcdutl/v3 v3.7.1/go.mod h1:tfgiL1/hF5sUd34iUMxs72/dV++Zyi0KtLXTzw2ua+U= +go.etcd.io/etcd/pkg/v3 v3.7.1 h1:DeaWSKMDDC7ZpCvxmlFRVwtVALtjcgtLKRNSoafzjn8= +go.etcd.io/etcd/pkg/v3 v3.7.1/go.mod h1:Kc2rflR05f4t5S3ygMKjbPrn2vcRnW36Om+ta1I2Yw4= +go.etcd.io/etcd/server/v3 v3.7.1 h1:USlUEiIw4fE8hXVL4CGTLRucbRjrZhOJilJLleCUpDE= +go.etcd.io/etcd/server/v3 v3.7.1/go.mod h1:ua3hv+hHkI9GtipFrRDtdHx4N/M7fev9DA7P6EF5uNg= go.etcd.io/raft/v3 v3.7.0 h1:BGzlwx07bLv8PW6OU5HObuz1y4hlPZUXA07pM1mPUh4= go.etcd.io/raft/v3 v3.7.0/go.mod h1:6gX6T2X907DjnjsFLODnTxba77stjs84W9gTTI0GUNA= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 h1:2yEATaop1/a1I4psnSLgWVPLWwCzkqWakgJy7xTDVy0= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0/go.mod h1:D7J12YRapIekYyPWgGPlA/23pRmpSEZC5xJC/TTLI9U= -go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= -go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= -go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= -go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= -go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= -go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= -go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= -go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= -go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= -go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0 h1:oECp5f+hN7nkwjU/8BxQ/q23bGPb8FIrD839owX222E= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0/go.mod h1:DqEFwLumhzMBDQv9PcWbyoDxHI/4lAk6CM4nJBH39sc= +go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= +go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo= +go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M= +go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s= +go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw= +go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA= +go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o= +go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA= +go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag= +go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -520,24 +520,25 @@ go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4= go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/exp v0.0.0-20260718201538-764159d718ef h1:LkZ48HFgy/TvhTI0bcWkjgFkgLyKUwcTbDjS0DUjw+A= golang.org/x/exp v0.0.0-20260718201538-764159d718ef/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= -golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= +golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -545,8 +546,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= -golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -578,8 +579,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -588,8 +589,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= -golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -598,14 +599,14 @@ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdI golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a h1:97PfJ4tCxY5C7NzzgGqQEMZmXbISdvSArNNEOoUGKBg= -google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a/go.mod h1:1brfde68Npq6+WA75c1EHWPijZEG1kMus61ygPZfn4A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a h1:qI/YMH1ep2qQtqcp00gMQyoU7mjvbhg88GJKCvfoLj0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= -google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= -google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= -google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/genproto/googleapis/api v0.0.0-20260810153831-ec0a7760b754 h1:dWeMvEJ3JhYgqSCAHUZZJgMUyfniiiCvDc72x5EqJP0= +google.golang.org/genproto/googleapis/api v0.0.0-20260810153831-ec0a7760b754/go.mod h1:q/3oV3jAi5vwelxsVAprMBC8BcM2zmNe+IjRGd+9/ks= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260810153831-ec0a7760b754 h1:k5CJw9e5ONCcA/u0webKt092npXuY+KeGh3Q8NAVf0g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260810153831-ec0a7760b754/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -624,26 +625,26 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= helm.sh/helm/v4 v4.2.3 h1:JEejtPE04+SvyRomOfgRXVxyJ/lude7eShio30oQr0Y= helm.sh/helm/v4 v4.2.3/go.mod h1:azI2XpxowOGXAgzeXcqyfskUmIfILqIcJxiFw1M6PuM= -k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY= -k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg= -k8s.io/apiextensions-apiserver v0.36.2 h1:3O5gqOj/dt2XWWbpMe+TXWpE9yU6pjM/tXxtHHJT/K4= -k8s.io/apiextensions-apiserver v0.36.2/go.mod h1:cL1tBWe8XSaP1H30iWKGo7hf6iAUUUJPEU70dskmAnA= -k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ= -k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4= -k8s.io/cli-runtime v0.36.2 h1:CconTvEeV4DJs4ZX3HQKCFbFRGsm6OtuBM9yjmMP2VM= -k8s.io/cli-runtime v0.36.2/go.mod h1:LddcjiMf4YlnHO7c1Y7rEtDqL84FyiYVLco7V679GUU= -k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI= -k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0= -k8s.io/component-base v0.36.2 h1:Z0VH80O7Ng0HDZnZj3WRR3urEGa0kTwmO8CwEwjVK1w= -k8s.io/component-base v0.36.2/go.mod h1:mGfFOA7Gwpdm1VW2cwSQYbiDIlz8GD2WGwH88QSeCyA= +k8s.io/api v0.37.0 h1:Z//Vj9N7RA/yS2sDmxyeo7h+RR4zbUrd2vrd3Z0TbB4= +k8s.io/api v0.37.0/go.mod h1:LKXgcJWMc+f4OLbP5SFR8rulEg07zZhpi/zMULiBImk= +k8s.io/apiextensions-apiserver v0.37.0 h1:zRMQ3+/LIE5oZ0tVvXwYHC+dIkSP5cjNWju7AZU1LOI= +k8s.io/apiextensions-apiserver v0.37.0/go.mod h1:HU0PfSBwchHL5iDau6jjt9zU6ryWkDDlaVUiq91NK80= +k8s.io/apimachinery v0.37.0 h1:Np2AbDtf8x6RDHiD8T9LbKJ9gaegeVNa8yNm5FuGKm0= +k8s.io/apimachinery v0.37.0/go.mod h1:RN3nhprFSCxOi5Selxd7oMTXOe/c+ZbcE7Im+TS2zkE= +k8s.io/cli-runtime v0.37.0 h1:U3XakUeirBQJMz5688r04z74SIHSE7V5SIZ6Ho5JyBM= +k8s.io/cli-runtime v0.37.0/go.mod h1:qiQMFkKwFFuPH6zy953On+nc3qfpEHAIDrJmAuRz5Vg= +k8s.io/client-go v0.37.0 h1:nsN31fy8wBySuZ+QRnKmrjRSQLOG2rvoGN0tKd12zhQ= +k8s.io/client-go v0.37.0/go.mod h1:FcGqw+Ll/gNQiq+nPGY1Oyt9y7SgDh1d3MW3RFDEbn0= +k8s.io/component-base v0.37.0 h1:3SdSa4+itMdFTDFTeR8CxKGmSTSMXFlKL4ky8OqjguM= +k8s.io/component-base v0.37.0/go.mod h1:LjOebp4R9y6LODWZQv102ZQxGheLcDO2ZJLAw6bbh4I= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A= k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I= -k8s.io/kubectl v0.36.2 h1:rpUGGpeL09XVOLep2yle5jrtk//JA1L6ZHfkQQtVEwk= -k8s.io/kubectl v0.36.2/go.mod h1:gVbQ3B/yb4bSR2ggQ7rd0W6icUSWs7sduH4e16Vii+0= -k8s.io/streaming v0.36.2 h1:NSKthPPg9UFSKsRauVJUVGH2Dvn8fhKmY4qrMkw/p98= -k8s.io/streaming v0.36.2/go.mod h1:z6fV3D+NVkoeqRMtWwlUZK6U17SY/LqNzOxWL6GyR/s= +k8s.io/kubectl v0.37.0 h1:cici6hiofx93ASldmprDmZF55SfhVt4o3HniltVLjTc= +k8s.io/kubectl v0.37.0/go.mod h1:RSeEl8e/yqDx6srG8Azr0uAtVPNIZljA0PNh9HCBcdg= +k8s.io/streaming v0.37.0 h1:iPBUZLZiKt5bV+lxJurASMOV07VuBhNpiwJt2//AWrM= +k8s.io/streaming v0.37.0/go.mod h1:APlJR26ZWRcVy5bIEj0QRrKUXROtBHPcxl2NT7EAzPU= k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 h1:jVkFFVfXdXP74B/zbO3hM3hpSFD0xvhQ5U686DPurkE= k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3/go.mod h1:M2s5JB1lIYP3jzZdorPLHXIPJzt9vv2muW5a6L9DtNM= sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4= diff --git a/pkg/commands/apply.go b/pkg/commands/apply.go index 9d27b535..b91eef47 100644 --- a/pkg/commands/apply.go +++ b/pkg/commands/apply.go @@ -458,7 +458,7 @@ func applyOneFileTemplateMode(configFile string, sidePatches, modelineTemplates // template-rendering mode. ctx is shaped for ApplyConfiguration on // every apply path: the auth branch sets `nodes` (plural, one // element) via openClientPerNodeAuth so apid resolves a single -// backend and helpers.ForEachResource can read the plural key from +// backend and pkg/engine's forEachResource can read the plural key from // inside template lookups; the insecure branch carries no node // metadata at all and the maintenance client dials a single endpoint // per call. @@ -521,7 +521,7 @@ func buildApplyClosure() applyFunc { // client.WithNode (singular) here is intentional and unrelated to // the auth template-rendering apply path's switch from WithNode to // WithNodes (openClientPerNodeAuth) — preflight performs a direct -// COSI Get against one resource, not a helpers.ForEachResource walk +// COSI Get against one resource, not a pkg/engine forEachResource walk // that reads the plural "nodes" metadata key. apid's COSI router // accepts the singular "node" key for single-target addressing (and // rejects the plural "nodes" key for any COSI method, regardless of @@ -710,8 +710,8 @@ func shouldRunDriftPreview(skip bool) bool { } // runPostApplyGate wires Phase 2B (post-apply state verification). -// Skipped on dry-run (no real apply) and on the staged/try/reboot -// apply modes: +// Skipped on dry-run (no real apply) and on the staged, try and +// reboot apply modes: // // - --mode=staged stores the new config as staged; the active // MachineConfig resource is unchanged until reboot, so a verify @@ -719,11 +719,13 @@ func shouldRunDriftPreview(skip bool) bool { // - --mode=try applies the config but auto-rolls back after the // configured timeout; verify would race against the rollback // timer and produce false positives. -// - --mode=reboot reboots the node after ApplyConfiguration +// - the REBOOT mode reboots the node after ApplyConfiguration // returns success; the COSI connection dies mid-verify and the // reader returns a transient error, which the gate would // surface as a blocker — a false positive for a successful -// reboot apply. +// reboot apply. No --mode spelling produces it since Talos +// v1.14 (see modeReboot below); the branch stays for the value +// itself, which the server still serves. // // All three modes have explicit contracts that diverge from "what // was sent is what is on the node now after success was reported" @@ -742,6 +744,15 @@ func runPostApplyGate(ctx context.Context, c *client.Client, sent []byte, nodeID return verifyAppliedState(ctx, cosiMachineConfigReader(c, applyCmdFlags.insecure), sent, nodeID, w, redactor) } +// modeReboot is the apply mode that reboots the node after ApplyConfiguration. +// Talos v1.14 deprecated the enum value and dropped "reboot" from the mode flag +// it registers, so no --mode spelling produces it anymore; the server still +// serves the value. The predicate below stays exhaustive over the enum so the +// mode keeps its skip-the-verify behaviour if upstream brings the spelling back. +// +//nolint:staticcheck // SA1019: deprecated upstream, still served; named once instead of suppressed per use. +const modeReboot = machineapi.ApplyConfigurationRequest_REBOOT + // shouldRunPostApplyVerify is the testable predicate for runPostApplyGate. // Returns false when the verify must be skipped for any reason listed in // runPostApplyGate's doc. @@ -753,7 +764,7 @@ func shouldRunPostApplyVerify(mode machineapi.ApplyConfigurationRequest_Mode, dr switch mode { case machineapi.ApplyConfigurationRequest_STAGED, machineapi.ApplyConfigurationRequest_TRY, - machineapi.ApplyConfigurationRequest_REBOOT, + modeReboot, // AUTO is skipped because Talos's apply-server promotes AUTO // to REBOOT internally when the change requires it (the // CanApplyImmediate check inside v1alpha1_server.go's AUTO @@ -815,7 +826,7 @@ type ( // openClientFunc opens a Talos client suitable for a single node and runs // action with it. Authenticated mode reuses one parent client and rotates // the node via single-element-slice gRPC metadata (client.WithNodes with -// one entry — the plural key is what helpers.ForEachResource and apid both +// one entry — the plural key is what pkg/engine's forEachResource and apid both // read, while FailIfMultiNodes still treats len("nodes") == 1 as // single-target). Insecure (maintenance) mode opens a fresh // single-endpoint client per node because Talos's maintenance client @@ -919,18 +930,18 @@ func openClientPerNodeMaintenance(fingerprints []string, mkClient maintenanceCli // openClientPerNodeAuth returns an openClientFunc that reuses one // authenticated client (the one withApplyClientBare opened above this -// callback) and rotates the addressed node via client.WithNodes on the +// callback) and rotates the addressed node via withNodesMetadata on the // per-iteration context, passing a single-element slice. The plural key -// is what Talos's helpers.ForEachResource reads inside template lookups -// (cmd/talosctl/pkg/talos/helpers/resources.go) — a singular "node" key +// is what forEachResource reads inside template lookups +// (pkg/engine/talos_helpers.go) — a singular "node" key // is invisible to it and the helper falls back to []string{""}, which // surfaces as `rpc error: code = Internal desc = invalid target ""` -// from inside template `lookup` calls. helpers.FailIfMultiNodes accepts +// from inside template `lookup` calls. failIfMultiNodes accepts // len("nodes") <= 1, so a single-element slice still satisfies the // multi-node guard while making lookups work. func openClientPerNodeAuth(parentCtx context.Context, c *client.Client) openClientFunc { return func(node string, action func(ctx context.Context, c *client.Client) error) error { - return action(client.WithNodes(parentCtx, node), c) + return action(withNodesMetadata(parentCtx, node), c) } } @@ -946,7 +957,7 @@ func openClientPerNodeAuth(parentCtx context.Context, c *client.Client) openClie // helper. // // The auth template-rendering apply path uses client.WithNodes -// (plural, single-element slice) so that helpers.ForEachResource and +// (plural, single-element slice) so that pkg/engine's forEachResource and // the apid backend resolver can both read the plural key from template // lookups; that ctx is therefore unsuitable for COSI reads as is. // @@ -1143,7 +1154,7 @@ func wrapWithNodeContext(action func(ctx context.Context, c *client.Client) erro nodes = configContext.Nodes } - ctx = client.WithNodes(ctx, nodes...) + ctx = withNodesMetadata(ctx, nodes...) return action(ctx, c) } diff --git a/pkg/commands/apply_test.go b/pkg/commands/apply_test.go index 997a1307..b854e6be 100644 --- a/pkg/commands/apply_test.go +++ b/pkg/commands/apply_test.go @@ -310,7 +310,7 @@ func TestWrapWithNodeContext_NoNodesNoClient(t *testing.T) { // nodesFromOutgoingCtx pulls per-iteration node identity out of gRPC // outgoing metadata. Production rotates nodes via client.WithNodes with a // single-element slice (the plural "nodes" key is what -// helpers.ForEachResource and apid both read); the singular "node" key is +// pkg/engine's forEachResource and apid both read); the singular "node" key is // also checked here to keep the helper resilient against fakes or future // callers that use client.WithNode directly. Tests that pin the metadata // contract assert against md.Get directly rather than going through this @@ -341,7 +341,7 @@ func nodesFromOutgoingCtx(ctx context.Context, t *testing.T) []string { // silent test coverage of an untouched code path. func fakeAuthOpenClient(parentCtx context.Context) openClientFunc { return func(node string, action func(ctx context.Context, c *client.Client) error) error { - return action(client.WithNodes(parentCtx, node), nil) + return action(withNodesMetadata(parentCtx, node), nil) } } @@ -734,11 +734,11 @@ func TestOpenClientPerNodeMaintenance_RestoresGlobalNodesOnError(t *testing.T) { // TestApplyTemplatesPerNode_AuthModeUsesPluralNodesMetadataKey pins the // gRPC metadata key the auth-mode opener writes. The auth template-rendering // path drives lookups inside engine.Render through Talos's -// helpers.ForEachResource, which reads only the plural "nodes" metadata key +// pkg/engine's forEachResource, which reads only the plural "nodes" metadata key // (cmd/talosctl/pkg/talos/helpers/resources.go) — when that key is empty the // helper falls back to []string{""} and issues an RPC with an empty target, // surfacing as "rpc error: code = Internal desc = invalid target". -// helpers.FailIfMultiNodes accepts len("nodes") <= 1, so a single-element +// failIfMultiNodes accepts len("nodes") <= 1, so a single-element // plural slice keeps the multi-node guard happy while making lookups work. // The singular "node" key, in contrast, is invisible to ForEachResource and // must never be used on the auth path. @@ -756,7 +756,7 @@ func TestApplyTemplatesPerNode_AuthModeUsesPluralNodesMetadataKey(t *testing.T) t.Fatal("expected outgoing metadata on per-iteration ctx") } if got := md.Get("nodes"); !slices.Equal(got, []string{node}) { - t.Errorf(`metadata key "nodes" = %v, want [%q] (single-element plural slice — what helpers.ForEachResource reads)`, got, node) + t.Errorf(`metadata key "nodes" = %v, want [%q] (single-element plural slice — what pkg/engine's forEachResource reads)`, got, node) } if got := md.Get("node"); len(got) != 0 { t.Errorf(`metadata key "node" must be unset on auth apply, got %v`, got) @@ -774,7 +774,7 @@ func TestApplyTemplatesPerNode_AuthModeUsesPluralNodesMetadataKey(t *testing.T) // TestCosiPreflightContext_StripsPluralAndAttachesSingular pins the // COSI preflight ctx contract: the auth template-rendering apply path // puts the target node under the plural "nodes" metadata key (so -// helpers.ForEachResource and apid's machine-API backend resolver can +// pkg/engine's forEachResource and apid's machine-API backend resolver can // read it), but Talos's apid director rejects every COSI method whose // outgoing context carries the plural key, regardless of slice // length. cosiPreflightContext rebuilds ctx with the singular "node" @@ -784,7 +784,7 @@ func TestApplyTemplatesPerNode_AuthModeUsesPluralNodesMetadataKey(t *testing.T) // never sees the mismatch warning the preflight exists to surface. func TestCosiPreflightContext_StripsPluralAndAttachesSingular(t *testing.T) { const node = testNodeAddrA - in := client.WithNodes(context.Background(), node) + in := withNodesMetadata(context.Background(), node) out, _, err := cosiPreflightContext(in) if err != nil { @@ -892,7 +892,7 @@ func TestCosiPreflightContext_NoMetadata_MultipleGlobalArgsNodes_NoFallback(t *t // rejection, version mismatch never surfaces) — the exact symptom // this helper exists to prevent on the single-node case. func TestCosiPreflightContext_RejectsMultiNodeCtx(t *testing.T) { - in := client.WithNodes(context.Background(), "a", "b") + in := withNodesMetadata(context.Background(), "a", "b") _, _, err := cosiPreflightContext(in) if err == nil { t.Fatal("expected error for multi-node outgoing ctx, got nil") diff --git a/pkg/commands/client_wrappers_test.go b/pkg/commands/client_wrappers_test.go new file mode 100644 index 00000000..b1150d1d --- /dev/null +++ b/pkg/commands/client_wrappers_test.go @@ -0,0 +1,157 @@ +// Copyright Cozystack Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "context" + "os" + "path/filepath" + "slices" + "syscall" + "testing" + "time" + + "google.golang.org/grpc/metadata" + + "github.com/siderolabs/talos/pkg/machinery/client" + clientconfig "github.com/siderolabs/talos/pkg/machinery/client/config" +) + +func withNodesReset(t *testing.T) { + t.Helper() + + orig := GlobalArgs.Nodes + t.Cleanup(func() { GlobalArgs.Nodes = orig }) +} + +// TestWithClientMaintenance_SpansEveryNode pins that the maintenance client +// carries every --nodes entry as an endpoint. client.WithMaintenanceMode narrows +// the client to the single node it is handed, so a regression here would silently +// reduce a multi-node insecure apply to one node. +func TestWithClientMaintenance_SpansEveryNode(t *testing.T) { + withNodesReset(t) + + GlobalArgs.Nodes = []string{"192.0.2.1", "192.0.2.2"} + + var got []string + + err := WithClientMaintenance(nil, func(_ context.Context, c *client.Client) error { + got = c.GetEndpoints() + + return nil + }) + if err != nil { + t.Fatalf("WithClientMaintenance: %v", err) + } + + if !slices.Equal(got, GlobalArgs.Nodes) { + t.Errorf("maintenance client endpoints = %v, want %v", got, GlobalArgs.Nodes) + } +} + +// TestWithClientMaintenance_RejectsMalformedFingerprint pins that a bad +// --cert-fingerprint fails the connection instead of silently dropping the +// pinning, which would leave the insecure connection unauthenticated. +func TestWithClientMaintenance_RejectsMalformedFingerprint(t *testing.T) { + withNodesReset(t) + + GlobalArgs.Nodes = []string{"192.0.2.1"} + + err := WithClientMaintenance([]string{"not-a-fingerprint"}, func(context.Context, *client.Client) error { + t.Error("action ran despite a malformed fingerprint") + + return nil + }) + if err == nil { + t.Fatal("expected an error for a malformed certificate fingerprint") + } +} + +// TestWithClientNoNodes_UsesContextEndpointsWithoutNodes pins the two properties +// of the locally built client: endpoints come from the named talosconfig +// context, and no node metadata is attached — callers that want nodes add them +// in their own layer. Talos v1.14 replaced the talosctl wrapper with a factory +// that refuses to build without nodes, so this construction is talm's own. +func TestWithClientNoNodes_UsesContextEndpointsWithoutNodes(t *testing.T) { + withNodesReset(t) + + cfg := &clientconfig.Config{ + Context: "present", + Contexts: map[string]*clientconfig.Context{ + "present": {Endpoints: []string{"192.0.2.1"}, Nodes: []string{"192.0.2.9"}}, + }, + } + + cfgPath := filepath.Join(t.TempDir(), "talosconfig") + if err := cfg.Save(cfgPath); err != nil { + t.Fatalf("save talosconfig fixture: %v", err) + } + + origTalosconfig, origCmdContext, origEndpoints, origSkipVerify := GlobalArgs.Talosconfig, GlobalArgs.CmdContext, GlobalArgs.Endpoints, SkipVerify + + t.Cleanup(func() { + GlobalArgs.Talosconfig, GlobalArgs.CmdContext, GlobalArgs.Endpoints, SkipVerify = origTalosconfig, origCmdContext, origEndpoints, origSkipVerify + }) + + GlobalArgs.Talosconfig = cfgPath + GlobalArgs.CmdContext = "present" + GlobalArgs.Endpoints = nil + GlobalArgs.Nodes = nil + SkipVerify = false + + var ( + endpoints []string + hasNodes bool + ) + + err := WithClientNoNodes(func(ctx context.Context, c *client.Client) error { + endpoints = c.GetEndpoints() + + md, ok := metadata.FromOutgoingContext(ctx) + hasNodes = ok && len(md.Get("nodes")) > 0 + + return nil + }) + if err != nil { + t.Fatalf("WithClientNoNodes: %v", err) + } + + if want := []string{"192.0.2.1"}; !slices.Equal(endpoints, want) { + t.Errorf("endpoints = %v, want %v from the talosconfig context", endpoints, want) + } + + if hasNodes { + t.Error("WithClientNoNodes attached node metadata; callers add nodes themselves") + } +} + +// TestSignalContext_CancelsOnSignal pins the first half of the interrupt +// contract: a signal cancels the context the client call runs under. The second +// half (a second Ctrl+C killing the process) follows from unregistering the +// handler, which Go's default disposition then handles. +func TestSignalContext_CancelsOnSignal(t *testing.T) { + ctx, stop := signalContext() + defer stop() + + if err := syscall.Kill(os.Getpid(), syscall.SIGTERM); err != nil { + t.Fatalf("raise SIGTERM: %v", err) + } + + select { + case <-ctx.Done(): + case <-time.After(5 * time.Second): + t.Fatal("context was not cancelled by SIGTERM") + } +} diff --git a/pkg/commands/completion.go b/pkg/commands/completion.go index 534a3736..f1520001 100644 --- a/pkg/commands/completion.go +++ b/pkg/commands/completion.go @@ -21,6 +21,7 @@ import ( "github.com/cozystack/talm/pkg/generated" "github.com/cozystack/talm/pkg/modeline" + "github.com/siderolabs/talos/cmd/talosctl/pkg/talos/helpers" "github.com/siderolabs/talos/pkg/machinery/client/config" "github.com/spf13/cobra" ) @@ -36,14 +37,26 @@ const ( nodesDirName = "nodes" ) -// applyModeOptions enumerates the apply / patch / edit `--mode` -// values upstream talosctl exposes via helpers.AddModeFlags. Pinned -// here rather than imported because upstream's keys live inside a -// per-call map, not a package-level constant — we cannot reflect -// them at completion time without instantiating the Mode flag. -// -//nolint:gochecknoglobals // immutable lookup table used by completeApplyMode at completion time. -var applyModeOptions = []string{"auto", "no-reboot", "reboot", "staged", "try"} +// applyModeOptions enumerates the apply / patch / edit `--mode` values upstream +// talosctl accepts. Upstream keeps them in a per-call map with no exported +// accessor, but Mode.Type() renders that map as its flag-usage string, so the +// list is read back from a throwaway registration instead of being restated +// here. A pinned copy silently drifts on a Talos bump: v1.14 dropped "reboot", +// and a stale copy would tab-complete a value the flag then rejects. +func applyModeOptions() []string { + var mode helpers.Mode + + helpers.AddModeFlags(&mode, &cobra.Command{}) + + // Guard against a future shape where Type() renders nothing: Split would + // hand back one empty string, which completion would offer as a value. + rendered := mode.Type() + if rendered == "" { + return nil + } + + return strings.Split(rendered, ", ") +} // completePresetNames implements shell completion for the `--preset` // flag of `talm init`: the available presets are baked into the @@ -63,7 +76,7 @@ func completePresetNames(_ *cobra.Command, _ []string, _ string) ([]string, cobr // completeApplyMode implements shell completion for the `--mode` // flag of `talm apply`. Fixed enum, no file fallback. func completeApplyMode(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - return applyModeOptions, cobra.ShellCompDirectiveNoFileComp + return applyModeOptions(), cobra.ShellCompDirectiveNoFileComp } // completeYAMLFiles implements shell completion for flags that diff --git a/pkg/commands/completion_test.go b/pkg/commands/completion_test.go index 7dbd7bbe..d0db8411 100644 --- a/pkg/commands/completion_test.go +++ b/pkg/commands/completion_test.go @@ -20,31 +20,37 @@ import ( "slices" "testing" + "github.com/siderolabs/talos/cmd/talosctl/pkg/talos/helpers" "github.com/spf13/cobra" ) -// TestComplete_ApplyMode_ReturnsFixedEnum pins the apply --mode -// completion: five upstream-supported values, no file fallback. -// Operators get tab-completed enums instead of meaningless file -// suggestions. -func TestComplete_ApplyMode_ReturnsFixedEnum(t *testing.T) { +// TestComplete_ApplyMode_OffersOnlyAcceptedValues pins the apply --mode +// completion against the flag itself: every suggestion has to parse, and no +// file fallback. Counting the values instead would keep passing after a Talos +// bump changes the set, which is how "reboot" survived in the suggestions +// after v1.14 stopped accepting it. +func TestComplete_ApplyMode_OffersOnlyAcceptedValues(t *testing.T) { got, directive := completeApplyMode(nil, nil, "") - if !slices.Equal(got, applyModeOptions) { - t.Errorf("--mode completion = %v, want %v", got, applyModeOptions) + if len(got) == 0 { + t.Fatal("--mode completion returned nothing") } - // Sanity: every value upstream's helpers.AddModeFlags - // registers must surface. The string forms are pinned in - // upstream's mode.go (modeAuto, modeNoReboot, …). - if len(got) != 5 { - t.Errorf("--mode completion must surface five upstream-supported modes; got %v", got) + + for _, value := range got { + var mode helpers.Mode + + helpers.AddModeFlags(&mode, &cobra.Command{}) + + if err := mode.Set(value); err != nil { + t.Errorf("--mode completion offers %q, which the flag rejects: %v", value, err) + } } - // Pin presence of the always-default mode by string so a - // future rename of applyModeOptions still produces a - // detectable failure mode. + + // The always-default mode, pinned by string so a rename still fails here. if !slices.Contains(got, "auto") { t.Errorf("--mode completion missing default mode; got %v", got) } + if directive != cobra.ShellCompDirectiveNoFileComp { t.Errorf("directive = %v, want ShellCompDirectiveNoFileComp", directive) } diff --git a/pkg/commands/contract_init_ux_test.go b/pkg/commands/contract_init_ux_test.go index 7536d50f..ba1657c9 100644 --- a/pkg/commands/contract_init_ux_test.go +++ b/pkg/commands/contract_init_ux_test.go @@ -516,3 +516,43 @@ func TestContract_InitRun_ForceBypassesPreCheck(t *testing.T) { t.Errorf("--force should bypass the pre-check, got: %v", err) } } + +// TestContract_InitRun_ProducesTalosconfig pins that a fresh init runs to +// completion and writes a usable talosconfig. +// +// The whole init path is covered by tests, but none of them reached the config +// bundle before this one: Talos v1.14 made generate reject an empty Kubernetes +// version, and `talm init` — the first command an operator runs — started +// failing with "kubernetes version must be specified" while the suite stayed +// green. +func TestContract_InitRun_ProducesTalosconfig(t *testing.T) { + withInitFlagsSnapshot(t) + withConfigSnapshot(t) + + dirAbs, _ := filepath.Abs(t.TempDir()) + t.Chdir(dirAbs) + Config.RootDir = dirAbs + Config.RootDirExplicit = true + Config.InitOptions.Version = "v0.0.0-test" + + initCmdFlags.preset = presetGeneric + initCmdFlags.name = "test-cluster" + initCmdFlags.force = false + initCmdFlags.encrypt = false + initCmdFlags.decrypt = false + initCmdFlags.update = false + initCmdFlags.image = "" + + if err := initCmd.RunE(initCmd, nil); err != nil { + t.Fatalf("init on an empty directory must succeed: %v", err) + } + + raw, err := os.ReadFile(filepath.Join(dirAbs, "talosconfig")) + if err != nil { + t.Fatalf("init did not write a talosconfig: %v", err) + } + + if !strings.Contains(string(raw), "test-cluster") { + t.Errorf("talosconfig does not carry the cluster name:\n%s", raw) + } +} diff --git a/pkg/commands/init.go b/pkg/commands/init.go index 6dfd6c63..05bec0e3 100644 --- a/pkg/commands/init.go +++ b/pkg/commands/init.go @@ -40,6 +40,7 @@ import ( "github.com/siderolabs/talos/pkg/machinery/config" "github.com/siderolabs/talos/pkg/machinery/config/generate" "github.com/siderolabs/talos/pkg/machinery/config/generate/secrets" + "github.com/siderolabs/talos/pkg/machinery/constants" "k8s.io/apimachinery/pkg/util/validation" ) @@ -682,7 +683,18 @@ var initCmd = &cobra.Command{ // Generate talosconfig only if it doesn't exist if !talosconfigFileExists { - configBundle, err := gen.GenerateConfigBundle(genOptions, clusterName, "https://192.168.0.1:6443", "", []string{}, []string{}, []string{}) + configBundle, err := gen.GenerateConfigBundle( + genOptions, + clusterName, + "https://192.168.0.1:6443", // dummy endpoint, not used for talosconfig + // Only the PKI and secrets are read back out of this bundle, so the + // Kubernetes version does not reach the project. Talos v1.14's + // generate refuses an empty one, hence the default. + constants.DefaultKubernetesVersion, + []string{}, + []string{}, + []string{}, + ) if err != nil { return errors.Wrap(err, "generating talos config bundle") } diff --git a/pkg/commands/preflight_apply_safety_test.go b/pkg/commands/preflight_apply_safety_test.go index 3c7062b7..4c38d24b 100644 --- a/pkg/commands/preflight_apply_safety_test.go +++ b/pkg/commands/preflight_apply_safety_test.go @@ -801,7 +801,7 @@ func TestShouldRunDriftPreview_ModeAgnostic(t *testing.T) { t.Parallel() modes := []machineapi.ApplyConfigurationRequest_Mode{ - machineapi.ApplyConfigurationRequest_REBOOT, + modeReboot, machineapi.ApplyConfigurationRequest_NO_REBOOT, machineapi.ApplyConfigurationRequest_AUTO, machineapi.ApplyConfigurationRequest_STAGED, @@ -854,7 +854,7 @@ func TestShouldRunPostApplyVerify_RespectsModeAndDryRun(t *testing.T) { {"no_reboot runs", machineapi.ApplyConfigurationRequest_NO_REBOOT, false, false, true}, // Modes that reach the node only intermittently or never on // ActiveID: skipped. - {"reboot skipped", machineapi.ApplyConfigurationRequest_REBOOT, false, false, false}, + {"reboot skipped", modeReboot, false, false, false}, {"staged skipped", machineapi.ApplyConfigurationRequest_STAGED, false, false, false}, {"try skipped", machineapi.ApplyConfigurationRequest_TRY, false, false, false}, // AUTO is skipped because Talos's apply-server promotes it to diff --git a/pkg/commands/preflight_upgrade_verify_test.go b/pkg/commands/preflight_upgrade_verify_test.go index a8cb1357..08bb51d8 100644 --- a/pkg/commands/preflight_upgrade_verify_test.go +++ b/pkg/commands/preflight_upgrade_verify_test.go @@ -27,34 +27,33 @@ import ( // TestShouldRunPostUpgradeVerify_SkipMatrix pins the predicate that // gates Phase 2C scheduling. The gate cannot produce a meaningful // result on --insecure (no auth COSI path) or --stage (new partition -// not yet booted); both must be skipped to avoid false-positive -// blockers. The skip flag overrides everything (operator opt-out). +// not yet booted), which must be skipped to avoid a false-positive +// blocker. The skip flag overrides everything (operator opt-out). +// +// Talos v1.14 removed upgrade's --insecure, which used to be the other skip. func TestShouldRunPostUpgradeVerify_SkipMatrix(t *testing.T) { t.Parallel() tests := []struct { - name string - insecure bool - staged bool - skip bool - want bool + name string + staged bool + skip bool + want bool }{ - {"default runs", false, false, false, true}, - {"--skip-post-upgrade-verify suppresses everything", false, false, true, false}, - {"--insecure skipped (no auth COSI)", true, false, false, false}, - {"--stage skipped (new partition not booted)", false, true, false, false}, - {"--insecure + --stage skipped", true, true, false, false}, - {"all-on skipped", true, true, true, false}, + {"default runs", false, false, true}, + {"--skip-post-upgrade-verify suppresses everything", false, true, false}, + {"--stage skipped (new partition not booted)", true, false, false}, + {"both skipped", true, true, false}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() - got := shouldRunPostUpgradeVerify(tc.insecure, tc.staged, tc.skip) + got := shouldRunPostUpgradeVerify(tc.staged, tc.skip) if got != tc.want { - t.Errorf("shouldRunPostUpgradeVerify(insecure=%v, staged=%v, skip=%v) = %v, want %v", - tc.insecure, tc.staged, tc.skip, got, tc.want) + t.Errorf("shouldRunPostUpgradeVerify(staged=%v, skip=%v) = %v, want %v", + tc.staged, tc.skip, got, tc.want) } }) } diff --git a/pkg/commands/root.go b/pkg/commands/root.go index d64fe794..395e8de7 100644 --- a/pkg/commands/root.go +++ b/pkg/commands/root.go @@ -18,6 +18,7 @@ import ( "context" "crypto/tls" "encoding/base64" + "fmt" "os" "os/signal" "path/filepath" @@ -52,10 +53,36 @@ var SkipVerify bool // talosconfig. var errContextNotFound = errors.New("context not found in talosconfig") -// signalContext returns a context cancelled on SIGINT/SIGTERM so a --skip-verify -// client connection can be interrupted cleanly, mirroring talosctl's own wrappers. +// signalContext returns a context cancelled on SIGINT/SIGTERM, mirroring the +// wrappers talosctl builds its own clients with. +// +// It unregisters the handler on the first signal, so a second Ctrl+C kills the +// process outright. signal.NotifyContext would keep the registration, and the +// second signal would land in a full channel and be discarded, leaving a stuck +// call with no way out from the keyboard. +// +// Follows siderolabs/talos pkg/cli/context.go, which is licensed under MPL-2.0: +// https://github.com/siderolabs/talos/blob/v1.14.0/pkg/cli/context.go func signalContext() (context.Context, context.CancelFunc) { - return signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + ctx, cancel := context.WithCancel(context.Background()) + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) + + go func() { + select { + case <-sigCh: + signal.Stop(sigCh) + fmt.Fprintln(os.Stderr, "Signal received, aborting, press Ctrl+C once again to abort immediately...") + cancel() + case <-ctx.Done(): + } + }() + + return ctx, func() { + signal.Stop(sigCh) + cancel() + } } // skipVerifyTLSConfig builds a TLS config that skips server-certificate @@ -154,8 +181,62 @@ func WithClientNoNodes(action func(context.Context, *client.Client) error, dialO return WithClientSkipVerify(action, dialOptions...) } - //nolint:wrapcheck // thin pass-through to talos global.Args; error already carries Talos context - return GlobalArgs.WithClientNoNodes(action, dialOptions...) + ctx, stop := signalContext() + defer stop() + + // Built on pkg/machinery/client rather than the talosctl wrapper: Talos + // v1.14 replaced that wrapper with a ClientFactory which refuses to + // construct without nodes, which is the one thing this function allows. + // + // The option set follows siderolabs/talos + // cmd/talosctl/pkg/talos/global/client.go (MPL-2.0): + // https://github.com/siderolabs/talos/blob/v1.13.7/cmd/talosctl/pkg/talos/global/client.go + cfg, err := clientconfig.Open(GlobalArgs.Talosconfig) + if err != nil { + return errors.Wrapf(err, "opening talosconfig %q", GlobalArgs.Talosconfig) + } + + opts := []client.OptionFunc{ + client.WithConfig(cfg), + client.WithDefaultGRPCDialOptions(), + client.WithGRPCDialOptions(dialOptions...), + client.WithSideroV1KeysDir(clientconfig.CustomSideroV1KeysDirPath(GlobalArgs.SideroV1KeysDir)), + } + + if GlobalArgs.CmdContext != "" { + opts = append(opts, client.WithContextName(GlobalArgs.CmdContext)) + } + + if len(GlobalArgs.Endpoints) > 0 { + opts = append(opts, client.WithEndpoints(GlobalArgs.Endpoints...)) + } + + if GlobalArgs.Cluster != "" { + opts = append(opts, client.WithCluster(GlobalArgs.Cluster)) + } + + c, err := client.New(ctx, opts...) + if err != nil { + return errors.Wrap(err, "constructing Talos client") + } + + defer func() { _ = c.Close() }() + + return action(ctx, c) +} + +// withNodesMetadata attaches the plural "nodes" key to the request context. +// +// Upstream deprecated client.WithNodes in favor of WithNode plus client-side +// multiplexing, but the plural key is what forEachResource and failIfMultiNodes +// read (pkg/engine/talos_helpers.go); with only the singular key a template +// `lookup` resolves against an empty target and fails at the RPC. Migrating +// means moving those two off the metadata, so the deprecated call is kept here +// as the single place that has to change. +// +//nolint:staticcheck // SA1019: see above — the plural key is load-bearing for template lookups. +func withNodesMetadata(ctx context.Context, nodes ...string) context.Context { + return client.WithNodes(ctx, nodes...) } // WithClient builds upon WithClientNoNodes to provide set of nodes on request context based on config & flags. @@ -174,7 +255,7 @@ func WithClient(action func(context.Context, *client.Client) error, dialOptions GlobalArgs.Nodes = configContext.Nodes } - ctx = client.WithNodes(ctx, GlobalArgs.Nodes...) + ctx = withNodesMetadata(ctx, GlobalArgs.Nodes...) return action(ctx, cli) }, @@ -183,9 +264,35 @@ func WithClient(action func(context.Context, *client.Client) error, dialOptions } // WithClientMaintenance wraps common code to initialize Talos client in maintenance (insecure mode). +// +// One client spans every node in GlobalArgs.Nodes, as the talosctl wrapper did +// before v1.14 hid it behind a per-node ClientFactory; callers that need a +// single-endpoint client narrow the list themselves (openClientPerNodeMaintenance). +// GlobalArgs.Nodes is read synchronously because that narrowing restores the +// saved list as soon as action returns. +// +// Follows the same upstream file as WithClientNoNodes above (MPL-2.0). func WithClientMaintenance(enforceFingerprints []string, action func(context.Context, *client.Client) error) error { - //nolint:wrapcheck // thin pass-through to talos global.Args; error already carries Talos context - return GlobalArgs.WithClientMaintenance(enforceFingerprints, action) + ctx, stop := signalContext() + defer stop() + + nodes := GlobalArgs.Nodes + + c, err := client.New(ctx, + client.WithDefaultGRPCDialOptions(), + // Taken for the insecure TLS config and the fingerprint pinning. Its node + // argument is inert here: options are applied in order, and WithEndpoints + // below overwrites the single endpoint it sets. + client.WithMaintenanceMode("", enforceFingerprints), + client.WithEndpoints(nodes...), + ) + if err != nil { + return errors.Wrap(err, "constructing maintenance client") + } + + defer func() { _ = c.Close() }() + + return action(ctx, c) } // skipVerifyClientOptions assembles the client options for a --skip-verify diff --git a/pkg/commands/skip_verify_test.go b/pkg/commands/skip_verify_test.go index e7efb8c9..f4998f66 100644 --- a/pkg/commands/skip_verify_test.go +++ b/pkg/commands/skip_verify_test.go @@ -146,10 +146,10 @@ func TestSkipVerifyTLSConfig_MismatchedKeyPair(t *testing.T) { // "present", points GlobalArgs at it while requesting the absent context // "absent", and toggles SkipVerify — restoring every mutated global on cleanup. // -// It is a routing probe: a wrapper that reaches the local WithClientSkipVerify -// fails with errContextNotFound before dialing, whereas one that falls through -// to upstream global.Args surfaces a different error. That lets a test assert -// which path a client wrapper took without needing a live node. +// It is a routing probe: a wrapper that reaches WithClientSkipVerify fails with +// errContextNotFound before dialing, whereas the plain path builds a client +// against the default context and surfaces a different error. That lets a test +// assert which path a client wrapper took without needing a live node. func stageMissingContextTalosconfig(t *testing.T, skipVerify bool) { t.Helper() diff --git a/pkg/commands/talosconfig.go b/pkg/commands/talosconfig.go index 3c494049..0da263b3 100644 --- a/pkg/commands/talosconfig.go +++ b/pkg/commands/talosconfig.go @@ -26,6 +26,7 @@ import ( machineconfig "github.com/siderolabs/talos/pkg/machinery/config" "github.com/siderolabs/talos/pkg/machinery/config/generate" "github.com/siderolabs/talos/pkg/machinery/config/generate/secrets" + "github.com/siderolabs/talos/pkg/machinery/constants" "github.com/spf13/cobra" "gopkg.in/yaml.v3" ) @@ -158,7 +159,9 @@ func regenerateTalosconfig() error { genOptions, clusterName, "https://192.168.0.1:6443", // dummy endpoint, not used for talosconfig - "", + // Kubernetes version is irrelevant for talosconfig (only PKI/secrets are + // extracted), but Talos v1.14's generate now requires a non-empty value. + constants.DefaultKubernetesVersion, []string{}, []string{}, []string{}, diff --git a/pkg/commands/talosctl_wrapper_test.go b/pkg/commands/talosctl_wrapper_test.go index 5a61ef64..5a98c77a 100644 --- a/pkg/commands/talosctl_wrapper_test.go +++ b/pkg/commands/talosctl_wrapper_test.go @@ -632,13 +632,19 @@ func TestWrapTalosCommand_RealCrashdumpPopulatesNodesFromControlPlane(t *testing } } -// TestWrapTalosCommand_RealMetaWritePropagatesInsecure pins the -// persistent-shorthand path: metaCmd.PersistentFlags() registers -// -i / --insecure with shorthand "i". Through the wrapper, both -// the long and short forms must resolve on `meta write`. Pinning -// the shorthand catches a future regression where the wrapper -// might copy the long flag but lose the shorthand attribute. -func TestWrapTalosCommand_RealMetaWritePropagatesInsecure(t *testing.T) { +// TestWrapTalosCommand_RealMetaPropagatesInsecure pins that the wrapper copies +// the upstream meta command's --insecure across, long form and -i shorthand +// both, so a future regression that copies the flag but drops the shorthand +// attribute is caught. +// +// It asserts on `meta`, not on `meta write`. Talos v1.14.0 moved --insecure +// from metaCmd.PersistentFlags() to metaCmd.Flags() when it introduced +// global.InsecureFlags, and a local flag does not reach a subcommand, so +// `talosctl meta write --insecure` stopped parsing upstream as well. talm +// mirrors upstream here rather than papering over it; compensating would mean +// talm accepting a flag talosctl rejects. Reported as +// https://github.com/siderolabs/talos/issues/14346. +func TestWrapTalosCommand_RealMetaPropagatesInsecure(t *testing.T) { var metaCmd *cobra.Command for _, cmd := range taloscommands.Commands { @@ -655,22 +661,23 @@ func TestWrapTalosCommand_RealMetaWritePropagatesInsecure(t *testing.T) { wrapped := wrapTalosCommand(metaCmd, "meta") - writeCmd, _, err := wrapped.Find([]string{"write"}) - if err != nil { - t.Fatalf("Find write under wrapped meta: %v", err) - } - // Long form. - if err := writeCmd.ParseFlags([]string{"--insecure"}); err != nil { - t.Fatalf("ParseFlags --insecure on wrapped meta write: %v", err) + if err := wrapped.ParseFlags([]string{"--insecure"}); err != nil { + t.Fatalf("ParseFlags --insecure on wrapped meta: %v", err) } - if writeCmd.Flags().Lookup("insecure") == nil { - t.Fatal("wrapped meta write must see --insecure from metaCmd.PersistentFlags()") + if wrapped.Flags().Lookup("insecure") == nil { + t.Fatal("wrapped meta must carry --insecure across from the upstream command") } // Short form. Re-parse to exercise the -i alias path. - if err := writeCmd.ParseFlags([]string{"-i"}); err != nil { - t.Errorf("ParseFlags -i on wrapped meta write: %v — shorthand attribute lost during copy?", err) + if err := wrapped.ParseFlags([]string{"-i"}); err != nil { + t.Errorf("ParseFlags -i on wrapped meta: %v — shorthand attribute lost during copy?", err) + } + + // The subcommands still exist and still do not see the parent's local flag, + // exactly as upstream leaves them. + if _, _, err := wrapped.Find([]string{"write"}); err != nil { + t.Fatalf("Find write under wrapped meta: %v", err) } } diff --git a/pkg/commands/template.go b/pkg/commands/template.go index c2e43164..7fb55b98 100644 --- a/pkg/commands/template.go +++ b/pkg/commands/template.go @@ -290,6 +290,7 @@ func runTemplate(ctx context.Context, tmpl func(ctx context.Context, c *client.C case templateCmdFlags.offline: return tmpl(ctx, nil) case templateCmdFlags.insecure: + //nolint:contextcheck // WithClientMaintenance owns its signal-rooted context, as the talosctl wrappers did return WithClientMaintenance(nil, tmpl) default: // WithClient handles --skip-verify (via WithClientNoNodes routing) and diff --git a/pkg/commands/upgrade_handler.go b/pkg/commands/upgrade_handler.go index d6a06046..b7d4436d 100644 --- a/pkg/commands/upgrade_handler.go +++ b/pkg/commands/upgrade_handler.go @@ -196,12 +196,11 @@ Post-upgrade sync (when the upgrade succeeds): // can overwrite the --image flag with the node's // currently-running install.image (the no-op-upgrade path), // which would mask the version mismatch Phase 2C exists to - // catch. --insecure and --stage are captured here too so + // catch. --stage is captured here too so // the post-upgrade gate's mode predicate sees what the // operator actually asked for, not whatever state talosctl // left in the flags afterwards. targetImage, _ := cmd.Flags().GetString("image") - insecure, _ := cmd.Flags().GetBool("insecure") staged, _ := cmd.Flags().GetBool("stage") // Execute original command @@ -225,8 +224,8 @@ Post-upgrade sync (when the upgrade succeeds): // and the operator's "successful" upgrade silently no-ops. // Skip predicate documents the cases where this gate cannot // produce a meaningful result. - if !shouldRunPostUpgradeVerify(insecure, staged, upgradeCmdFlags.skipPostUpgradeVerify) { - // Verify skipped (operator opt-out / insecure / staged). + if !shouldRunPostUpgradeVerify(staged, upgradeCmdFlags.skipPostUpgradeVerify) { + // Verify skipped (operator opt-out / staged). // Still sync node bodies: the RPC was acked, and skipping // the verify is an explicit operator choice — the body // must track what talosctl was asked to install so the @@ -266,13 +265,6 @@ Post-upgrade sync (when the upgrade succeeds): // scheduling. The gate cannot produce a meaningful result when: // // - --skip-post-upgrade-verify is set (operator opt-out). -// - --insecure was passed to upgrade: the maintenance / pre-auth -// connection cannot reach the auth-only COSI ctx WithClient -// builds. Pre-fix, the gate fell through to WithClient and -// either silently surrendered on "version unreadable" or -// connected to an unrelated node from talosconfig context. -// Mirrors cosiMachineConfigReader's insecure-path branch in -// pkg/commands/preflight_apply_safety.go. // - --stage was passed to upgrade: talosctl --stage writes the // new image to the inactive partition without activating it; // activation happens on the next reboot. runtime.Version still @@ -280,15 +272,15 @@ Post-upgrade sync (when the upgrade succeeds): // booted — a guaranteed false-positive blocker without this // skip. Mirrors shouldRunPostApplyVerify's STAGED case in // pkg/commands/apply.go. -func shouldRunPostUpgradeVerify(insecure, staged, skip bool) bool { +// +// Talos v1.14 dropped upgrade's --insecure, so there is no longer a +// maintenance-connection case to skip for; it was the third condition here +// until the flag stopped being registered upstream. +func shouldRunPostUpgradeVerify(staged, skip bool) bool { if skip { return false } - if insecure { - return false - } - if staged { return false } diff --git a/pkg/engine/contract_lookup_classify_test.go b/pkg/engine/contract_lookup_classify_test.go index 38df2983..fec9e811 100644 --- a/pkg/engine/contract_lookup_classify_test.go +++ b/pkg/engine/contract_lookup_classify_test.go @@ -115,7 +115,7 @@ func TestClassifyLookupError_Resource_InvalidArgument(t *testing.T) { } } -// Contract: NotFound from the helpers.ForEachResource return value +// Contract: NotFound from the forEachResource return value // (ResolveResourceKind couldn't find the kind in the target Talos // version) classifies as Resource, NOT Unknown. The per-node // callback filters NotFound for missing instances, so any NotFound diff --git a/pkg/engine/contract_lookup_retry_test.go b/pkg/engine/contract_lookup_retry_test.go index cfe3f293..29839e1c 100644 --- a/pkg/engine/contract_lookup_retry_test.go +++ b/pkg/engine/contract_lookup_retry_test.go @@ -279,7 +279,7 @@ func TestRetryWithFailFast_BoundedTime(t *testing.T) { // Contract: firstLookupError surfaces per-node multierror failures // to the retry classifier, which closes the gap that would otherwise // leave the "brief partition against one node in a multi-node -// lookup" case unretried. helpers.ForEachResource itself returns nil +// lookup" case unretried. forEachResource itself returns nil // when only callback (per-node) errors occurred; pre-fix the // closure returned that nil, retryWithFailFast exited successfully, // and the multiErr was wrapped+hinted without a second attempt. diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 30109ac7..12b03556 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -29,8 +29,6 @@ import ( "helm.sh/helm/v4/pkg/chart/v2/loader" "helm.sh/helm/v4/pkg/strvals" - "github.com/siderolabs/talos/cmd/talosctl/pkg/talos/helpers" - "github.com/siderolabs/talos/pkg/machinery/client" "github.com/siderolabs/talos/pkg/machinery/config" "github.com/siderolabs/talos/pkg/machinery/config/bundle" @@ -39,6 +37,7 @@ import ( "github.com/siderolabs/talos/pkg/machinery/config/generate" "github.com/siderolabs/talos/pkg/machinery/config/generate/secrets" "github.com/siderolabs/talos/pkg/machinery/config/machine" + "github.com/siderolabs/talos/pkg/machinery/constants" ) // Options encapsulates all parameters necessary for rendering. @@ -159,8 +158,8 @@ func FullConfigProcess(opts Options, patches []string) (*bundle.Bundle, machine. // Updating parameters after applying patches machineType := configBundle.ControlPlaneCfg.Machine().Type() - clusterName := configBundle.ControlPlaneCfg.Cluster().Name() - clusterEndpoint := configBundle.ControlPlaneCfg.Cluster().Endpoint() + clusterName := configBundle.ControlPlaneCfg.K8sClusterConfig().ClusterName() + clusterEndpoint := configBundle.ControlPlaneCfg.K8sClusterConfig().ClusterEndpoint() if machineType == machine.TypeUnknown { machineType = machine.TypeWorker @@ -193,6 +192,24 @@ func FullConfigProcess(opts Options, patches []string) (*bundle.Bundle, machine. return configBundle, machineType, nil } +// kubeVersion returns the Kubernetes version (without the leading "v") for the +// config bundle, falling back to the version this binary's machinery was built +// against when unset. +// +// Talos v1.14's config/generate errors on an empty version, where earlier +// machinery emitted no image fields at all and left every component to the +// node's own default. The fallback is therefore a behaviour change for a project +// that pins nothing: it gets this binary's Kubernetes version instead of the +// node's. Both shipped presets pin the key, and docs/configuration/talos-versions.md +// tells operators to do the same. +func kubeVersion(v string) string { + if v == "" { + v = constants.DefaultKubernetesVersion + } + + return strings.TrimPrefix(v, "v") +} + // InitializeConfigBundle initializes a Talos configuration bundle from opts. // //nolint:gocritic // hugeParam: Options is the package's public facing configuration carrier; converting this to a pointer would propagate the change across every caller in pkg/commands and break the API for external consumers. @@ -222,7 +239,7 @@ func InitializeConfigBundle(opts Options) (*bundle.Bundle, error) { &bundle.InputOptions{ ClusterName: opts.ClusterName, Endpoint: opts.Endpoint, - KubeVersion: strings.TrimPrefix(opts.KubernetesVersion, "v"), + KubeVersion: kubeVersion(opts.KubernetesVersion), GenOptions: genOptions, }, ), @@ -1595,7 +1612,7 @@ func Render(ctx context.Context, c *client.Client, opts Options) ([]byte, error) cmdName = cmdNameTalm } - err := helpers.FailIfMultiNodes(ctx, cmdName) + err := failIfMultiNodes(ctx, cmdName) if err != nil { return nil, errors.Wrap(err, "checking node selector") } @@ -1880,7 +1897,7 @@ func applyPatchesAndRenderConfig(opts Options, configPatches []string) ([]byte, configBundleOpts := []bundle.Option{ bundle.WithInputOptions( &bundle.InputOptions{ - KubeVersion: strings.TrimPrefix(opts.KubernetesVersion, "v"), + KubeVersion: kubeVersion(opts.KubernetesVersion), GenOptions: genOptions, }, ), @@ -1912,8 +1929,8 @@ func applyPatchesAndRenderConfig(opts Options, configPatches []string) ([]byte, } machineType := configBundle.ControlPlaneCfg.Machine().Type() - clusterName := configBundle.ControlPlaneCfg.Cluster().Name() - clusterEndpoint := configBundle.ControlPlaneCfg.Cluster().Endpoint() + clusterName := configBundle.ControlPlaneCfg.K8sClusterConfig().ClusterName() + clusterEndpoint := configBundle.ControlPlaneCfg.K8sClusterConfig().ClusterEndpoint() if machineType == machine.TypeUnknown { machineType = machine.TypeWorker @@ -1929,7 +1946,7 @@ func applyPatchesAndRenderConfig(opts Options, configPatches []string) ([]byte, &bundle.InputOptions{ ClusterName: clusterName, Endpoint: clusterEndpoint.String(), - KubeVersion: strings.TrimPrefix(opts.KubernetesVersion, "v"), + KubeVersion: kubeVersion(opts.KubernetesVersion), GenOptions: genOptions, }, ), @@ -2121,10 +2138,10 @@ func newLookupFunction(ctx context.Context, c *client.Client, commandName string var resources []map[string]any - // Signature is fixed by helpers.ForEachResource; the callback + // Signature is fixed by forEachResource; the callback // always returns nil because per-item errors are accumulated // into multiErr / passed through for retry classification. - //nolint:unparam // callback shape fixed by helpers.ForEachResource API + //nolint:unparam // callback shape fixed by forEachResource API callbackResource := func(_ context.Context, _ string, r resource.Resource, callError error) error { if callError != nil { // Ignore NotFound and PermissionDenied errors - resource doesn't exist or is not accessible @@ -2163,7 +2180,7 @@ func newLookupFunction(ctx context.Context, c *client.Client, commandName string // half-collected partial result from a failed attempt does not // leak into the next one. // - // helpers.ForEachResource routes per-node dial failures (the + // forEachResource routes per-node dial failures (the // dominant transient class — a single node briefly partitioned // from the rest of a multi-node lookup) through callbackResource // as callError, where they land in multiErr and ForEachResource @@ -2199,7 +2216,7 @@ func newLookupFunction(ctx context.Context, c *client.Client, commandName string multiErr = nil resources = resources[:0] - return firstLookupError(helpers.ForEachResource(ctx, c, callbackRD, callbackResource, namespace, kind, docID), multiErr) + return firstLookupError(forEachResource(ctx, c, callbackRD, callbackResource, namespace, kind, docID), multiErr) }, shouldRetry, defaultRetryPolicy()) if attemptErr != nil { return map[string]any{}, wrapLookupError(attemptErr, kind, namespace, docID, endpoints, commandName) diff --git a/pkg/engine/lookup_classify.go b/pkg/engine/lookup_classify.go index 0de3be56..ab5dbd92 100644 --- a/pkg/engine/lookup_classify.go +++ b/pkg/engine/lookup_classify.go @@ -106,7 +106,7 @@ func classifyLookupError(err error) lookupErrorClass { return lookupErrAuthn case codes.Internal, codes.InvalidArgument, codes.FailedPrecondition, codes.NotFound, codes.Unimplemented: // NotFound and Unimplemented at this level come from the - // ResolveResourceKind path inside helpers.ForEachResource — + // ResolveResourceKind path inside forEachResource — // the operator asked for a resource kind the target Talos // version doesn't know about. The per-node callback already // filters NotFound for missing instances (engine.go), so any @@ -150,7 +150,7 @@ func classifyLookupError(err error) lookupErrorClass { // right remedy for the classified failure mode. // // Returns nil when err is nil so call sites can use it -// unconditionally after `helpers.ForEachResource`. +// unconditionally after `forEachResource`. func wrapLookupError(err error, kind, namespace, docID string, endpoints []string, commandName string) error { if err == nil { return nil @@ -304,7 +304,7 @@ func interAttemptBackoff(attempt int) time.Duration { } // firstLookupError picks the most operator-relevant error from the -// two paths through which `helpers.ForEachResource` reports failure: +// two paths through which `forEachResource` reports failure: // the direct return value (resource-definition resolution against // the first endpoint) and the per-node multierror that accumulates // callback failures (per-node dial issues against the rest of a diff --git a/pkg/engine/render_test.go b/pkg/engine/render_test.go index f3b1ce8d..e19487a1 100644 --- a/pkg/engine/render_test.go +++ b/pkg/engine/render_test.go @@ -24,7 +24,6 @@ import ( "testing" helmEngine "github.com/cozystack/talm/pkg/engine/helm" - "github.com/siderolabs/talos/pkg/machinery/client" "helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/chart/v2/loader" ) @@ -4103,7 +4102,7 @@ func TestRenderFailIfMultiNodes_UsesCommandName(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx := client.WithNodes(context.Background(), "10.0.0.1", "10.0.0.2") + ctx := withNodesMetadata(context.Background(), "10.0.0.1", "10.0.0.2") opts := Options{ Offline: false, CommandName: tt.commandName, @@ -4122,7 +4121,7 @@ func TestRenderFailIfMultiNodes_UsesCommandName(t *testing.T) { // If a caller passes "talm apply", the error must not carry any // other subcommand name — historically the call site here emitted // "talm template" unconditionally. - ctx := client.WithNodes(context.Background(), "10.0.0.1", "10.0.0.2") + ctx := withNodesMetadata(context.Background(), "10.0.0.1", "10.0.0.2") opts := Options{Offline: false, CommandName: "talm apply"} _, err := Render(ctx, nil, opts) if err == nil { diff --git a/pkg/engine/talos_helpers.go b/pkg/engine/talos_helpers.go new file mode 100644 index 00000000..a35b0371 --- /dev/null +++ b/pkg/engine/talos_helpers.go @@ -0,0 +1,146 @@ +package engine + +import ( + "context" + "errors" + "fmt" + + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/resource/meta" + "github.com/cosi-project/runtime/pkg/state" + "google.golang.org/grpc/metadata" + + "github.com/siderolabs/talos/pkg/machinery/client" +) + +// Talos v1.14.0 dropped FailIfMultiNodes and ForEachResource from +// cmd/talosctl/pkg/talos/helpers: talosctl inlined the resource loop into its +// own get command and left no exported replacement. Both are thin wrappers over +// the public client API, so talm carries its own, the same way it carries +// --skip-verify since the fork was dropped. Behaviour matches what the helpers +// did before they were dropped, which is what the callers in engine.go expect. +// +// These follow the structure of siderolabs/talos +// cmd/talosctl/pkg/talos/helpers/resources.go (the resource walk) and +// checks.go (the multi-node guard), both licensed under MPL-2.0: +// https://github.com/siderolabs/talos/tree/v1.13.7/cmd/talosctl/pkg/talos/helpers + +// ErrMultiNodeUnsupported is returned for a command that only makes sense +// against a single node when the context names more than one. +var ErrMultiNodeUnsupported = errors.New("command is not supported with multiple nodes") + +// ErrNoResourceType is returned when no resource type was given to walk. +var ErrNoResourceType = errors.New("not enough arguments: at least 1 is expected") + +// failIfMultiNodes reports an error when the context carries more than one node +// in its outgoing metadata. +func failIfMultiNodes(ctx context.Context, command string) error { + md, ok := metadata.FromOutgoingContext(ctx) + if !ok { + return nil + } + + if len(md.Get("nodes")) <= 1 { + return nil + } + + return fmt.Errorf("%w: %q", ErrMultiNodeUnsupported, command) +} + +// forEachResource resolves a resource kind and runs callback for every resource +// of that kind, on every node named in the context's outgoing metadata. +// +// The kind is resolved once, against the first node: a resource definition is +// cluster-wide, so which node answers does not matter. A per-node lookup failure +// is handed to the callback rather than returned, which is what lets a caller +// report an unreachable node and carry on with the rest; an error the callback +// itself returns stops the walk. +func forEachResource( + ctx context.Context, + c *client.Client, + callbackRD func(rd *meta.ResourceDefinition) error, + callback func(ctx context.Context, hostname string, r resource.Resource, callError error) error, + namespace string, + args ...string, +) error { + if len(args) == 0 { + return ErrNoResourceType + } + + resourceType := args[0] + + var resourceID string + + if len(args) > 1 { + resourceID = args[1] + } + + md, _ := metadata.FromOutgoingContext(ctx) + + nodes := md.Get("nodes") + if len(nodes) == 0 { + nodes = []string{""} + } + + resourceDefinition, err := c.ResolveResourceKind(client.WithNode(ctx, nodes[0]), &namespace, resourceType) + if err != nil { + return fmt.Errorf("resolving resource kind %q: %w", resourceType, err) + } + + if callbackRD != nil { + if cbErr := callbackRD(resourceDefinition); cbErr != nil { + return cbErr + } + } + + resourceType = resourceDefinition.TypedSpec().Type + + for _, node := range nodes { + if err := walkNodeResources(ctx, c, callback, namespace, resourceType, resourceID, node); err != nil { + return err + } + } + + return nil +} + +// walkNodeResources runs callback over one node's resources of a single kind: +// the one named by resourceID, or every resource of the kind when it is empty. +func walkNodeResources( + ctx context.Context, + c *client.Client, + callback func(ctx context.Context, hostname string, r resource.Resource, callError error) error, + namespace, resourceType, resourceID, node string, +) error { + nodeCtx := ctx + if node != "" { + nodeCtx = client.WithNode(ctx, node) + } + + if resourceID != "" { + r, callErr := c.COSI.Get( + nodeCtx, + resource.NewMetadata(namespace, resourceType, resourceID, resource.VersionUndefined), + state.WithGetUnmarshalOptions(state.WithSkipProtobufUnmarshal()), + ) + + return callback(ctx, node, r, callErr) + } + + items, callErr := c.COSI.List( + nodeCtx, + resource.NewMetadata(namespace, resourceType, "", resource.VersionUndefined), + state.WithListUnmarshalOptions(state.WithSkipProtobufUnmarshal()), + ) + if callErr != nil { + return callback(ctx, node, nil, callErr) + } + + for _, r := range items.Items { + if err := callback(ctx, node, r, nil); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/engine/talos_helpers_test.go b/pkg/engine/talos_helpers_test.go new file mode 100644 index 00000000..f28378b1 --- /dev/null +++ b/pkg/engine/talos_helpers_test.go @@ -0,0 +1,150 @@ +package engine + +import ( + "context" + "errors" + "slices" + "testing" + + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/state" + "github.com/cosi-project/runtime/pkg/state/impl/inmem" + "github.com/cosi-project/runtime/pkg/state/impl/namespaced" + + "github.com/siderolabs/talos/pkg/machinery/client" + "github.com/siderolabs/talos/pkg/machinery/resources/network" +) + +// withNodesMetadata attaches the plural "nodes" key the way the talm commands do, +// which is the key forEachResource and failIfMultiNodes read back. Upstream +// deprecated client.WithNodes in favor of the singular WithNode; until those two +// helpers move off the metadata, tests have to write what they read. +// +//nolint:staticcheck // SA1019: see above — the plural key is what the helpers under test consume. +func withNodesMetadata(ctx context.Context, nodes ...string) context.Context { + return client.WithNodes(ctx, nodes...) +} + +// collectWalk runs the walker over one node and returns what the callback saw: +// "node/resource-id" per resource, plus the per-node errors handed to it rather +// than returned. +func collectWalk(t *testing.T, st state.State, namespace, resourceType, resourceID, node string) ([]string, []error) { + t.Helper() + + c := &client.Client{COSI: st} + + var ( + seen []string + callErrs []error + ) + + err := walkNodeResources(context.Background(), c, + func(_ context.Context, hostname string, r resource.Resource, callErr error) error { + if callErr != nil { + callErrs = append(callErrs, callErr) + + return nil + } + + seen = append(seen, hostname+"/"+r.Metadata().ID()) + + return nil + }, + namespace, resourceType, resourceID, node) + if err != nil { + t.Fatalf("walkNodeResources: %v", err) + } + + return seen, callErrs +} + +// An empty resource ID means "every resource of this kind", and the node name +// is reported back for each one. Template lookups rely on both: the chart asks +// for a whole kind and attributes results per node. +func TestWalkNodeResources_ListsEveryResourceWithNode(t *testing.T) { + t.Parallel() + + st := state.WrapCore(namespaced.NewState(inmem.Build)) + + for _, id := range []string{"eth0", "eth1"} { + if err := st.Create(context.Background(), network.NewHostnameSpec(network.NamespaceName, id)); err != nil { + t.Fatalf("seed %q: %v", id, err) + } + } + + seen, callErrs := collectWalk(t, st, network.NamespaceName, network.HostnameSpecType, "", "node0") + + if len(callErrs) != 0 { + t.Fatalf("unexpected callback errors: %v", callErrs) + } + + want := []string{"node0/eth0", "node0/eth1"} + if !slices.Equal(seen, want) { + t.Errorf("walk produced %v, want %v", seen, want) + } +} + +// A missing resource reaches the callback as callError rather than aborting the +// walk. That is what lets a caller ignore NotFound for one node and carry on +// with the rest, which is how template lookups treat absent resources. +func TestWalkNodeResources_RoutesMissingResourceToCallback(t *testing.T) { + t.Parallel() + + st := state.WrapCore(namespaced.NewState(inmem.Build)) + + seen, callErrs := collectWalk(t, st, network.NamespaceName, network.HostnameSpecType, "absent", "node0") + + if len(seen) != 0 { + t.Errorf("walk reported resources that do not exist: %v", seen) + } + + if len(callErrs) != 1 { + t.Fatalf("expected exactly one callback error, got %v", callErrs) + } +} + +// failIfMultiNodes guards the commands that only make sense against one node. +// One node is fine, several are not, and a context with no node metadata at all +// (an offline render) must not trip the guard. +func TestFailIfMultiNodes(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + nodes []string + wantErr bool + }{ + {"no metadata", nil, false}, + {"one node", []string{"node0"}, false}, + {"two nodes", []string{"node0", "node1"}, true}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + if tc.nodes != nil { + ctx = withNodesMetadata(ctx, tc.nodes...) + } + + err := failIfMultiNodes(ctx, "talm template") + if gotErr := err != nil; gotErr != tc.wantErr { + t.Fatalf("failIfMultiNodes(%v) error = %v, want error: %v", tc.nodes, err, tc.wantErr) + } + + if tc.wantErr && !errors.Is(err, ErrMultiNodeUnsupported) { + t.Errorf("error = %v, want ErrMultiNodeUnsupported", err) + } + }) + } +} + +// forEachResource needs a resource kind to walk. The guard fires before the +// client is touched, which is what makes it testable without one. +func TestForEachResource_RequiresResourceType(t *testing.T) { + t.Parallel() + + err := forEachResource(context.Background(), nil, nil, nil, "ns") + if !errors.Is(err, ErrNoResourceType) { + t.Fatalf("forEachResource with no kind = %v, want ErrNoResourceType", err) + } +} From 4b552a9d951d27c901b8e332c7e5a871aa7edb71 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 11 Sep 2026 10:29:54 +0300 Subject: [PATCH 02/10] fix(applycheck): accept veth ends as links the apply creates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Talos v1.14 added VethConfig, whose two ends (name and peer.name) are links the config brings into existence. The walker had no handler for the kind, so a document referencing either end — a Layer2VIPConfig on a veth, a VLAN parented to one — was validated against the node's existing links and blocked the apply with "declared link not found" on a config Talos accepts. Record both ends the way DummyLinkConfig and LinkAliasConfig are recorded. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin --- docs/manual-test-plan.md | 1 + pkg/applycheck/refs.go | 20 +++++++++++++++++-- pkg/applycheck/refs_test.go | 38 +++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/docs/manual-test-plan.md b/docs/manual-test-plan.md index 22580f80..9dcb93ff 100644 --- a/docs/manual-test-plan.md +++ b/docs/manual-test-plan.md @@ -926,6 +926,7 @@ The Section C entries above smoke the apply pipe end-to-end. This section is the | Typoed VLAN parent | Add `VLANConfig{name: ens5.99, parent: ghost0, vlanID: 99}` (YAML key is `parent`, NOT `link` — `vlan.go ParentLinkConfig`) | Blocker on `parent: ghost0`; `name: ens5.99` not flagged (new VLAN) | | Typoed bridge slave | Add `BridgeConfig{name: br99, links: [ghost0]}` (YAML key is `links`, NOT `ports` — `bridge.go BridgeLinks`) | Blocker on `ghost0` only; `br99` not flagged | | Typoed Layer2VIP link | Set `vipLink: ghost0` in values | Blocker on `link: ghost0` | +| VIP on a veth end | Add `VethConfig{name: veth0, peer: {name: veth1}}` plus `Layer2VIPConfig{link: veth1}` | No finding — both ends are links the apply creates (Talos v1.14 kind) | | Legacy v1.11 interface | `machine.network.interfaces[].interface: eth9999` | Blocker; same hint shape | #### Disk references diff --git a/pkg/applycheck/refs.go b/pkg/applycheck/refs.go index cbcac261..71713cbe 100644 --- a/pkg/applycheck/refs.go +++ b/pkg/applycheck/refs.go @@ -207,8 +207,11 @@ var multidocHandlers = map[string]multidocHandler{ // resolves. WireguardConfig also creates a link but is deliberately // absent here: it belongs to the parallel net-addr walker, and the // two dispatch maps must stay disjoint or a kind gets double-walked. - "DummyLinkConfig": handleCreatorOnly, - "LinkAliasConfig": handleCreatorOnly, + "DummyLinkConfig": handleCreatorOnly, + "LinkAliasConfig": handleCreatorOnly, + // A veth pair brings both of its ends into existence (veth.go MetaName + // plus VethPeerConfig.name), so both are recorded rather than validated. + "VethConfig": handleVeth, "UserVolumeConfig": handleUserVolume, } @@ -246,6 +249,19 @@ func handleCreatorOnly(refs []Ref, doc map[string]any, basePath string) []Ref { return appendCreatedRef(refs, doc, basePath) } +// handleVeth records both ends of a veth pair as links this config creates: +// the doc's own .name and its .peer.name. +func handleVeth(refs []Ref, doc map[string]any, basePath string) []Ref { + refs = appendCreatedRef(refs, doc, basePath) + + peer, ok := doc["peer"].(map[string]any) + if !ok { + return refs + } + + return appendCreatedRef(refs, peer, basePath+".peer") +} + // handleListOnly records the doc's own .name as a link this config // creates, then emits one existing-link ref per entry of its list-valued // slaves/ports. Used for BondConfig and BridgeConfig: the .name diff --git a/pkg/applycheck/refs_test.go b/pkg/applycheck/refs_test.go index 5e528d14..c939ef33 100644 --- a/pkg/applycheck/refs_test.go +++ b/pkg/applycheck/refs_test.go @@ -495,3 +495,41 @@ kind: LinkConfig }) } } + +// TestWalkRefs_v1_14_VethCreatesBothEnds pins that a VethConfig seeds both +// ends of the pair into the created-link set, so a document elsewhere in the +// same apply may reference either one. Talos v1.14 added the kind; before it +// was registered the walker ignored it, and a VIP or VLAN pointing at a veth +// end failed validation as a missing link on a config Talos accepts. +func TestWalkRefs_v1_14_VethCreatesBothEnds(t *testing.T) { + t.Parallel() + + body := `apiVersion: v1alpha1 +kind: VethConfig +name: veth0 +peer: + name: veth1 +--- +apiVersion: v1alpha1 +kind: Layer2VIPConfig +link: veth1 +ip: 192.0.2.10 +` + refs, err := applycheck.WalkRefs([]byte(body)) + if err != nil { + t.Fatalf("WalkRefs: %v", err) + } + + for _, end := range []string{"veth0", "veth1"} { + if _, ok := findRef(refs, applycheck.RefKindLinkCreated, end); !ok { + t.Errorf("veth end %q not recorded as a created link, got refs=%+v", end, refs) + } + } + + findings := applycheck.ValidateRefs(refs, applycheck.HostSnapshot{Links: []string{"ens5"}}) + for i := range findings { + if findings[i].IsBlocker() { + t.Errorf("veth-backed VIP blocked the apply: %+v", findings[i]) + } + } +} From 5b4ede34ac6fedd990eacbef8d9d06cf3ca8a618 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 11 Sep 2026 10:51:16 +0300 Subject: [PATCH 03/10] feat(applycheck): resolve the links BGP and wireguard documents name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Talos v1.14 added BGPInstanceConfig, which names existing links in three places: the .advertise[] entries whose addresses are originated into BGP, the neighbors running unnumbered sessions over a link, and the VRF the session runs in. A typo in any of them reached the node unvalidated. Machinery does not resolve the VRF either — its Validate only reads the field to reject BFD in a VRF — so the miss surfaced on the node at runtime, where it takes the whole BGP projection down. Registering those references needs the documents that create links to be known, which is what exposed two older gaps: WireguardConfig and VRFConfig created a link but were never recorded as doing so, and a VLAN or VIP layered on one was rejected as a missing link on a config Talos accepts. Both are recorded now, and VRFConfig's own .links[] are resolved the way a bond's are. WireguardConfig is consequently dispatched by both walkers, which stays safe because the link side only records a created link and emits nothing that gets validated. The test that pinned the two dispatch maps as disjoint now declares that overlap by name instead. Machinery accepts a link alias wherever it accepts a link name: all three BGP fields say so, VRFConfig.links[] says so, and BondConfig.links[] said so before any of them. The host snapshot the apply validates against carried only the LinkStatus IDs, so validating the new references would have blocked a config Talos accepts. The snapshot now collects each link's alias and altnames too, which fixes the bond case at the same time. The instance's own name is not a link and stays unvalidated. Signed-off-by: Aleksei Sviridkin Assisted-by: LLM --- docs/manual-test-plan.md | 3 + docs/operations/safety-gates.md | 8 +- pkg/applycheck/refs.go | 73 +++++++++- pkg/applycheck/refs_netaddr.go | 2 +- pkg/applycheck/refs_netaddr_test.go | 30 ++-- pkg/applycheck/refs_test.go | 170 +++++++++++++++++++++- pkg/applycheck/validate_test.go | 31 ++-- pkg/commands/preflight_apply_safety.go | 22 ++- pkg/commands/preflight_link_names_test.go | 83 +++++++++++ 9 files changed, 374 insertions(+), 48 deletions(-) create mode 100644 pkg/commands/preflight_link_names_test.go diff --git a/docs/manual-test-plan.md b/docs/manual-test-plan.md index 9dcb93ff..feb8fb03 100644 --- a/docs/manual-test-plan.md +++ b/docs/manual-test-plan.md @@ -927,6 +927,9 @@ The Section C entries above smoke the apply pipe end-to-end. This section is the | Typoed bridge slave | Add `BridgeConfig{name: br99, links: [ghost0]}` (YAML key is `links`, NOT `ports` — `bridge.go BridgeLinks`) | Blocker on `ghost0` only; `br99` not flagged | | Typoed Layer2VIP link | Set `vipLink: ghost0` in values | Blocker on `link: ghost0` | | VIP on a veth end | Add `VethConfig{name: veth0, peer: {name: veth1}}` plus `Layer2VIPConfig{link: veth1}` | No finding — both ends are links the apply creates (Talos v1.14 kind) | +| Typoed BGP neighbor link | Add `BGPInstanceConfig{name: bgp0, neighbors: [{link: ghost0}]}` | Blocker on `neighbors[0].link`; `bgp0` not flagged (names the instance, not a link) | +| BGP advertising a link the apply creates | Add `WireguardConfig{name: wg0}` plus `BGPInstanceConfig{advertise: [wg0]}` | No finding — wireguard, VRF, veth, bond, bridge and VLAN documents all create links the rest of the config may reference | +| Link named by its alias | On a node whose `talosctl get links` shows an alias or altname for a NIC, reference that alias from `BondConfig.links[]`, `VRFConfig.links[]`, `BGPInstanceConfig.advertise[]` or `neighbors[].link` | No finding — machinery accepts an alias wherever it accepts a link name, so the snapshot carries aliases and altnames alongside IDs | | Legacy v1.11 interface | `machine.network.interfaces[].interface: eth9999` | Blocker; same hint shape | #### Disk references diff --git a/docs/operations/safety-gates.md b/docs/operations/safety-gates.md index ebda7394..e0c65ade 100644 --- a/docs/operations/safety-gates.md +++ b/docs/operations/safety-gates.md @@ -11,17 +11,19 @@ ## 1. Declared-resource existence -Before sending the config to the node, the gate walks the rendered MachineConfig, extracts every reference to a host-side resource (network links from v1.12 multi-doc — `LinkConfig.name`, `BondConfig.links[]`, `VLANConfig.parent`, `BridgeConfig.links[]`, `Layer2VIPConfig.link`, `HCloudVIPConfig.link`, `DHCPv4Config.name` / `DHCPv6Config.name` / `EthernetConfig.name`; v1.11 legacy `machine.network.interfaces[].interface`; install disk via `machine.install.disk` literal or `machine.install.diskSelector`; `UserVolumeConfig.provisioning.diskSelector`), and verifies each against the node's COSI `LinkStatus`/`Disk` snapshots. A reference that doesn't resolve fails the apply with a `[blocker]` line listing the available names so the typo or migration miss is fixable from the values without re-running discovery. +Before sending the config to the node, the gate walks the rendered MachineConfig, extracts every reference to a host-side resource (network links from v1.12 multi-doc — `LinkConfig.name`, `BondConfig.links[]`, `VLANConfig.parent`, `BridgeConfig.links[]`, `VRFConfig.links[]`, `Layer2VIPConfig.link`, `HCloudVIPConfig.link`, `DHCPv4Config.name` / `DHCPv6Config.name` / `EthernetConfig.name`, and from v1.14 `BGPInstanceConfig.advertise[]` and `BGPInstanceConfig.neighbors[].link`; v1.11 legacy `machine.network.interfaces[].interface`; install disk via `machine.install.disk` literal or `machine.install.diskSelector`; `UserVolumeConfig.provisioning.diskSelector`), and verifies each against the node's COSI `LinkStatus`/`Disk` snapshots. A reference that doesn't resolve fails the apply with a `[blocker]` line listing the available names so the typo or migration miss is fixable from the values without re-running discovery. Disk selectors must match at least one (non-readonly, non-CDROM, non-virtual) disk — zero matches block, multiple matches warn (install picks the first). ### Virtual links this apply creates -Virtual-link-creator documents (`BondConfig.name`, `VLANConfig.name`, `BridgeConfig.name`, `WireguardConfig.name`, `DummyLinkConfig.name`, `LinkAliasConfig.name`) are intentionally NOT validated against existing links — those `.name` fields describe new virtual links the apply is creating, not references to pre-existing host resources. Five of them (`BondConfig`, `VLANConfig`, `BridgeConfig`, `DummyLinkConfig`, `LinkAliasConfig`) additionally register their `.name` as a link this apply brings into existence, so a `VLANConfig.parent`, `BondConfig.links[]` or `Layer2VIPConfig.link` pointing at one of them resolves rather than blocking. That is what makes a `network.extraLinks` bond usable on first apply, before the node carries it. +Virtual-link-creator documents are intentionally NOT validated against existing links — their `.name` describes a new virtual link the apply is creating, not a reference to a pre-existing host resource. They also register that name as a link this apply brings into existence, so a `VLANConfig.parent`, `BondConfig.links[]`, `Layer2VIPConfig.link` or `BGPInstanceConfig.advertise[]` pointing at one of them resolves rather than blocking. That is what makes a `network.extraLinks` bond usable on first apply, before the node carries it. + +The set is talm's own, chosen per document by what its `.name` means: `BondConfig`, `BridgeConfig`, `VLANConfig`, `DummyLinkConfig`, `LinkAliasConfig`, `WireguardConfig`, `VRFConfig`, and `VethConfig` — the last registering both its `.name` and its `.peer.name`, since a veth pair creates both ends. `LinkConfig` is deliberately not among them: its `.name` addresses an existing NIC, so it is validated rather than recorded. A `LinkAliasConfig` name ending in `%d` is registered as a pattern rather than a literal, because Talos expands it into one sequential alias per matched link (`net0`, `net1`, …) — so a reference to `net0` resolves while the literal `net%d`, which never exists on the node, is not treated as a link. -`WireguardConfig` is the exception: it is handled by the net-addr walker (for its `peers[].endpoint`), which stays disjoint from the link walker, so it does not register its name — a VLAN or VIP layered on a wireguard link still blocks on first apply and needs `--skip-resource-validation` until the link exists. +`WireguardConfig` is walked twice, by design: the net-addr walker checks its `peers[].endpoint`, and the link walker records the link it creates. That is safe because the link side emits only a created-link entry, which seeds the known-links set and produces no finding of its own — so the two walkers cannot report the same mistake twice. ### Address and CIDR checks diff --git a/pkg/applycheck/refs.go b/pkg/applycheck/refs.go index 71713cbe..ddc241c5 100644 --- a/pkg/applycheck/refs.go +++ b/pkg/applycheck/refs.go @@ -165,6 +165,10 @@ func walkV1Alpha1Root(refs []Ref, machine map[string]any, basePath string) []Ref return refs } +// wireguardConfigKind is dispatched by both walkers: here for the link it +// creates, and in the net-addr walker for its peer endpoints. +const wireguardConfigKind = "WireguardConfig" + // multidocHandler emits the refs for one v1.12 multi-doc kind. Handlers are // registered in multidocHandlers and dispatched by walkMultidocKind; this // keeps walkMultidocKind a flat lookup instead of a giant switch. @@ -204,15 +208,23 @@ var multidocHandlers = map[string]multidocHandler{ // created. Their .name is the new resource, not an existing-link // reference, so it is recorded as created rather than validated — // that way a VLAN or VIP pointing at one of them in the same config - // resolves. WireguardConfig also creates a link but is deliberately - // absent here: it belongs to the parallel net-addr walker, and the - // two dispatch maps must stay disjoint or a kind gets double-walked. + // resolves. "DummyLinkConfig": handleCreatorOnly, "LinkAliasConfig": handleCreatorOnly, + // WireguardConfig and VRFConfig create a link the rest of the config may + // point at. WireguardConfig is also dispatched by the net-addr walker, which + // checks its peer endpoints; the two walkers produce different kinds of + // output, and a created-link ref is never validated, so neither duplicates + // the other. VRFConfig additionally names the links it enslaves. + wireguardConfigKind: handleCreatorOnly, + "VRFConfig": handleListOnly("links"), // A veth pair brings both of its ends into existence (veth.go MetaName // plus VethPeerConfig.name), so both are recorded rather than validated. - "VethConfig": handleVeth, - "UserVolumeConfig": handleUserVolume, + "VethConfig": handleVeth, + // An unnumbered BGP session names an existing link or alias to run over + // (bgp.go NeighborLinkConfig `yaml:"link"`); .name is the instance, not a link. + "BGPInstanceConfig": handleBGPInstance, + "UserVolumeConfig": handleUserVolume, } // walkMultidocKind handles v1.12 multi-doc shapes by kind discriminator. @@ -262,14 +274,59 @@ func handleVeth(refs []Ref, doc map[string]any, basePath string) []Ref { return appendCreatedRef(refs, peer, basePath+".peer") } +// handleBGPInstance emits an existing-link ref for every link the instance +// names: the .advertise[] entries whose addresses are originated into BGP, and +// each neighbor running an unnumbered session over a link. Numbered neighbors +// carry .address instead and reference no link, and .name is the instance. +// +// .vrf names the VRF the session runs in. Machinery does not resolve it — +// BGPInstanceConfig.Validate only reads the field to reject BFD in a VRF — and +// the miss surfaces only in the node's controller at runtime, where it takes the +// whole BGP projection down. A VRF device is a link, and VRFConfig registers its +// name as one this apply creates, so the reference resolves the same way the +// others do. +func handleBGPInstance(refs []Ref, doc map[string]any, basePath string) []Ref { + refs = appendListRefs(refs, doc, "advertise", basePath+".advertise") + + if vrf, ok := doc["vrf"].(string); ok && vrf != "" { + refs = append(refs, Ref{Kind: RefKindLink, Name: vrf, Source: basePath + ".vrf"}) + } + + neighbors, ok := doc["neighbors"].([]any) + if !ok { + return refs + } + + for i, entry := range neighbors { + neighbor, ok := entry.(map[string]any) + if !ok { + continue + } + + link, ok := neighbor["link"].(string) + if !ok || link == "" { + continue + } + + refs = append(refs, Ref{ + Kind: RefKindLink, + Name: link, + Source: fmt.Sprintf("%s.neighbors[%d].link", basePath, i), + }) + } + + return refs +} + // handleListOnly records the doc's own .name as a link this config // creates, then emits one existing-link ref per entry of its list-valued -// slaves/ports. Used for BondConfig and BridgeConfig: the .name +// slaves/ports. Used for BondConfig, BridgeConfig and VRFConfig: the .name // describes a virtual link the apply brings into existence — validating // it against the node would reject every bond on its first apply, while // recording it lets a VLAN or VIP elsewhere in the same config resolve -// against it. The .links[] members are pre-existing physical NICs that -// must already be present. +// against it. The .links[] members must resolve too, either to a link on +// the node or to one another document in the same apply creates. A member may +// name a link by its alias, which is why the host snapshot carries aliases. func handleListOnly(listKey string) multidocHandler { return func(refs []Ref, doc map[string]any, basePath string) []Ref { refs = appendCreatedRef(refs, doc, basePath) diff --git a/pkg/applycheck/refs_netaddr.go b/pkg/applycheck/refs_netaddr.go index 03db0ba3..3bb99844 100644 --- a/pkg/applycheck/refs_netaddr.go +++ b/pkg/applycheck/refs_netaddr.go @@ -34,7 +34,7 @@ type netAddrHandler func(doc map[string]any, basePath string) []Finding var multidocNetAddrHandlers = map[string]netAddrHandler{ "StaticHostConfig": handleStaticHostConfigName, "NetworkRuleConfig": handleNetworkRuleConfigIngress, - "WireguardConfig": handleWireguardEndpoints, + wireguardConfigKind: handleWireguardEndpoints, } // WalkNetAddrFindings parses the rendered MachineConfig bytes and diff --git a/pkg/applycheck/refs_netaddr_test.go b/pkg/applycheck/refs_netaddr_test.go index ee160556..0df8148f 100644 --- a/pkg/applycheck/refs_netaddr_test.go +++ b/pkg/applycheck/refs_netaddr_test.go @@ -362,19 +362,29 @@ func TestWalkNetAddrFindings_RealSchema_NetworkRuleConfig(t *testing.T) { } } -// TestMultidocNetAddrHandlers_NoOverlapWithRefHandlers pins the -// dispatch-map disjointness contract: net-addr handlers run in a -// parallel walker, so a kind that appears in BOTH maps would get -// double-walked (one finding from each pipeline) — silent -// duplication. None of the three net-addr kinds (StaticHostConfig, -// NetworkRuleConfig, WireguardConfig) are in multidocHandlers today; -// pin that contract so a future entry doesn't create overlap. -func TestMultidocNetAddrHandlers_NoOverlapWithRefHandlers(t *testing.T) { +// TestMultidocNetAddrHandlers_OverlapEmitsNoValidatedRefs pins what the two +// dispatch maps must not do together: report the same thing twice. +// +// A kind may appear in both — WireguardConfig does, because the net-addr walker +// checks its peer endpoints while the ref walker records the link it creates. +// That is safe only while the ref side emits nothing that gets validated: a +// created-link ref seeds the known-links set and produces no finding of its own. +// A kind that emitted an existing-link ref from one map and a finding from the +// other would surface the same mistake twice. +func TestMultidocNetAddrHandlers_OverlapEmitsNoValidatedRefs(t *testing.T) { t.Parallel() for kind := range multidocNetAddrHandlers { - if _, exists := multidocHandlers[kind]; exists { - t.Errorf("kind %q registered in BOTH multidocHandlers (ref-based) and multidocNetAddrHandlers (syntactic) — duplicate findings; pick one pipeline", kind) + handler, overlaps := multidocHandlers[kind] + if !overlaps { + continue + } + + refs := handler(nil, map[string]any{"name": "probe0"}, "doc[0]") + for _, ref := range refs { + if ref.Kind != RefKindLinkCreated { + t.Errorf("kind %q is in both dispatch maps and emits a validated ref (%v) — the same mistake would be reported twice", kind, ref.Kind) + } } } } diff --git a/pkg/applycheck/refs_test.go b/pkg/applycheck/refs_test.go index c939ef33..6609e7bd 100644 --- a/pkg/applycheck/refs_test.go +++ b/pkg/applycheck/refs_test.go @@ -512,8 +512,8 @@ peer: --- apiVersion: v1alpha1 kind: Layer2VIPConfig +name: 192.0.2.10 link: veth1 -ip: 192.0.2.10 ` refs, err := applycheck.WalkRefs([]byte(body)) if err != nil { @@ -533,3 +533,171 @@ ip: 192.0.2.10 } } } + +// TestWalkRefs_v1_14_BGPNeighborLink pins that an unnumbered BGP neighbor's +// link is validated like any other existing-link reference, while a numbered +// neighbor (which carries an address instead) contributes nothing. Talos v1.14 +// added the kind. +func TestWalkRefs_v1_14_BGPNeighborLink(t *testing.T) { + t.Parallel() + + body := `apiVersion: v1alpha1 +kind: BGPInstanceConfig +name: bgp0 +localASN: 64496 +neighbors: + - link: ghost0 + peerASN: 64497 + - address: 192.0.2.1 + peerASN: 64498 +` + refs, err := applycheck.WalkRefs([]byte(body)) + if err != nil { + t.Fatalf("WalkRefs: %v", err) + } + + if _, ok := findRef(refs, applycheck.RefKindLink, "ghost0"); !ok { + t.Errorf("unnumbered neighbor link not validated, got refs=%+v", refs) + } + + if _, ok := findRef(refs, applycheck.RefKindLink, "bgp0"); ok { + t.Error("instance name leaked as a link ref; it names the BGP instance, not a link") + } +} + +// TestWalkRefs_v1_14_BGPAdvertiseLinks pins that the links an instance +// originates addresses from are validated too. They are existing links, the +// same shape as BondConfig.links, and a typo there otherwise reaches the node. +func TestWalkRefs_v1_14_BGPAdvertiseLinks(t *testing.T) { + t.Parallel() + + body := `apiVersion: v1alpha1 +kind: BGPInstanceConfig +name: bgp0 +localASN: 64496 +advertise: + - ghost0 + - ens5 +` + refs, err := applycheck.WalkRefs([]byte(body)) + if err != nil { + t.Fatalf("WalkRefs: %v", err) + } + + for _, link := range []string{"ghost0", "ens5"} { + if _, ok := findRef(refs, applycheck.RefKindLink, link); !ok { + t.Errorf("advertise link %q not validated, got refs=%+v", link, refs) + } + } +} + +// TestWalkRefs_v1_14_BGPAdvertiseAcceptsCreatedLinks pins that a BGP instance +// may advertise links the same apply creates. Wireguard overlays and VRF +// devices are the common cases, and both are link-creating document kinds +// upstream, so blocking them would reject a config Talos accepts. +func TestWalkRefs_v1_14_BGPAdvertiseAcceptsCreatedLinks(t *testing.T) { + t.Parallel() + + body := `apiVersion: v1alpha1 +kind: WireguardConfig +name: wg0 +--- +apiVersion: v1alpha1 +kind: VRFConfig +name: vrf-blue +--- +apiVersion: v1alpha1 +kind: BGPInstanceConfig +name: bgp0 +localASN: 64496 +advertise: + - wg0 + - vrf-blue +` + refs, err := applycheck.WalkRefs([]byte(body)) + if err != nil { + t.Fatalf("WalkRefs: %v", err) + } + + findings := applycheck.ValidateRefs(refs, applycheck.HostSnapshot{Links: []string{"eth0"}}) + for i := range findings { + if findings[i].IsBlocker() { + t.Errorf("advertising a link this apply creates must not block: %+v", findings[i]) + } + } +} + +// TestWalkRefs_v1_14_VRFLinksValidated pins the other half of the VRF handler: +// the links a VRF enslaves must exist, the same as a bond's or a bridge's. +func TestWalkRefs_v1_14_VRFLinksValidated(t *testing.T) { + t.Parallel() + + body := `apiVersion: v1alpha1 +kind: VRFConfig +name: vrf-blue +links: + - ghost0 +` + refs, err := applycheck.WalkRefs([]byte(body)) + if err != nil { + t.Fatalf("WalkRefs: %v", err) + } + + if _, ok := findRef(refs, applycheck.RefKindLink, "ghost0"); !ok { + t.Errorf("an enslaved link is not validated, got refs=%+v", refs) + } + + if _, ok := findRef(refs, applycheck.RefKindLink, "vrf-blue"); ok { + t.Error("the VRF's own name leaked as an existing-link ref; this apply creates it") + } +} + +// TestWalkRefs_v1_14_BGPVRFValidated pins that the VRF a session runs in is +// resolved like any other link reference. Machinery does not check it, and the +// node only notices at runtime, where the miss takes the BGP projection down. +func TestWalkRefs_v1_14_BGPVRFValidated(t *testing.T) { + t.Parallel() + + body := `apiVersion: v1alpha1 +kind: BGPInstanceConfig +name: bgp0 +localASN: 64496 +vrf: ghost-vrf +` + refs, err := applycheck.WalkRefs([]byte(body)) + if err != nil { + t.Fatalf("WalkRefs: %v", err) + } + + if _, ok := findRef(refs, applycheck.RefKindLink, "ghost-vrf"); !ok { + t.Errorf("the VRF reference is not validated, got refs=%+v", refs) + } +} + +// A VRF created by the same apply satisfies the reference, so declaring both +// documents together is not a blocker. +func TestWalkRefs_v1_14_BGPVRFAcceptsCreatedVRF(t *testing.T) { + t.Parallel() + + body := `apiVersion: v1alpha1 +kind: VRFConfig +name: vrf-blue +--- +apiVersion: v1alpha1 +kind: BGPInstanceConfig +name: bgp0 +localASN: 64496 +vrf: vrf-blue +` + refs, err := applycheck.WalkRefs([]byte(body)) + if err != nil { + t.Fatalf("WalkRefs: %v", err) + } + + findings := applycheck.ValidateRefs(refs, applycheck.HostSnapshot{Links: []string{"ens5"}}) + for i := range findings { + if findings[i].IsBlocker() { + t.Errorf("a VRF this apply creates must satisfy the reference: %+v", findings[i]) + } + } +} diff --git a/pkg/applycheck/validate_test.go b/pkg/applycheck/validate_test.go index 1955d656..8fd2f4d8 100644 --- a/pkg/applycheck/validate_test.go +++ b/pkg/applycheck/validate_test.go @@ -648,13 +648,15 @@ parent: bond9 } } -// WireguardConfig is deliberately the exception to the created-link union: -// it belongs to the net-addr walker (which validates peers[].endpoint) and -// stays out of the link dispatch map, so it does NOT register its .name as -// a created link. A VLAN or VIP layered on a wireguard link therefore still -// blocks on first apply. This pins that boundary so the README claim and -// the code cannot drift. -func TestValidateRefs_WireguardCreatedLinkDoesNotSatisfyReferences(t *testing.T) { +// A wireguard link is created by the apply, exactly like a bond or a dummy: +// a wireguard link is one the apply brings into existence, not one the node is +// expected to already carry. A VLAN parented to it, or a VIP on it, is a +// config Talos accepts, so neither may block. +// +// WireguardConfig is dispatched by both walkers — the net-addr one validates its +// peer endpoints — which is safe because the ref side only records the created +// link and emits nothing that gets validated. +func TestValidateRefs_WireguardCreatedLinkSatisfiesReferences(t *testing.T) { t.Parallel() rendered := []byte(`apiVersion: v1alpha1 @@ -680,21 +682,10 @@ link: wg0 findings := applycheck.ValidateRefs(refs, applycheck.HostSnapshot{Links: []string{"eth0"}}) - var parentBlocked, linkBlocked bool for i := range findings { - f := &findings[i] - if !f.IsBlocker() || !strings.Contains(f.Ref.Name, "wg0") { - continue - } - if strings.HasSuffix(f.Ref.Source, ".parent") { - parentBlocked = true + if findings[i].IsBlocker() && strings.Contains(findings[i].Ref.Name, "wg0") { + t.Errorf("a reference to a wireguard link created by this apply must not block: %+v", findings[i]) } - if strings.HasSuffix(f.Ref.Source, ".link") { - linkBlocked = true - } - } - if !parentBlocked || !linkBlocked { - t.Errorf("a VLAN parent and a VIP link on a wireguard-created link must both still block, got %+v", findings) } } diff --git a/pkg/commands/preflight_apply_safety.go b/pkg/commands/preflight_apply_safety.go index 4b03e614..eb844911 100644 --- a/pkg/commands/preflight_apply_safety.go +++ b/pkg/commands/preflight_apply_safety.go @@ -18,6 +18,8 @@ import ( "context" "fmt" "io" + "iter" + "slices" "strings" "time" @@ -600,6 +602,20 @@ func readWithFreshTimeout[T any](parent context.Context, timeout time.Duration, return op(ctx) } +// snapshotLinkNames collects every name a link answers to: its ID, its alias +// and its altnames. Machinery documents the link-name fields of BondConfig, +// VRFConfig and BGPInstanceConfig as accepting an alias in place of the ID, so +// a snapshot holding only IDs blocks an apply Talos would accept. +func snapshotLinkNames(links iter.Seq[*network.LinkStatus]) []string { + var names []string + + for link := range links { + names = append(names, slices.Collect(network.AllLinkNames(link))...) + } + + return names +} + // cosiLinksDisksReader returns a linksDisksReader backed by the node's // COSI state. Both LinkStatus (network namespace) and Disk (block // namespace) are NonSensitive, so the maintenance / insecure path can @@ -618,11 +634,7 @@ func cosiLinksDisksReader(c *client.Client) linksDisksReader { } snapshot := applycheck.HostSnapshot{ - Links: make([]string, 0, links.Len()), - } - - for link := range links.All() { - snapshot.Links = append(snapshot.Links, link.Metadata().ID()) + Links: snapshotLinkNames(links.All()), } disks, err := readWithFreshTimeout(ctx, preflightCOSIReadTimeout, func(ctx context.Context) (safe.List[*block.Disk], error) { diff --git a/pkg/commands/preflight_link_names_test.go b/pkg/commands/preflight_link_names_test.go new file mode 100644 index 00000000..b3d70031 --- /dev/null +++ b/pkg/commands/preflight_link_names_test.go @@ -0,0 +1,83 @@ +// Copyright Cozystack Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "slices" + "testing" + + "github.com/cozystack/talm/pkg/applycheck" + "github.com/siderolabs/talos/pkg/machinery/resources/network" +) + +func TestSnapshotLinkNames_IncludesAliasAndAltNames(t *testing.T) { + t.Parallel() + + plain := network.NewLinkStatus(network.NamespaceName, "eth1") + + aliased := network.NewLinkStatus(network.NamespaceName, "eth0") + aliased.TypedSpec().Alias = "uplink0" + aliased.TypedSpec().AltNames = []string{"enp0s1", "slot-3"} + + got := snapshotLinkNames(slices.Values([]*network.LinkStatus{plain, aliased})) + + want := []string{"eth1", "eth0", "uplink0", "enp0s1", "slot-3"} + if !slices.Equal(got, want) { + t.Fatalf("snapshotLinkNames() = %v, want %v", got, want) + } +} + +// TestSnapshotLinkNames_ResolvesAliasedLinkRefs chains the snapshot the apply +// builds to the walker that consumes it: machinery lets BGPInstanceConfig and +// VRFConfig name a link by its alias, and the apply must not block that. +func TestSnapshotLinkNames_ResolvesAliasedLinkRefs(t *testing.T) { + t.Parallel() + + const rendered = `apiVersion: v1alpha1 +kind: BGPInstanceConfig +name: bgp0 +advertise: + - uplink0 +neighbors: + - link: uplink0 +vrf: uplink0 +--- +apiVersion: v1alpha1 +kind: VRFConfig +name: vrf0 +links: + - uplink0 +` + + refs, err := applycheck.WalkRefs([]byte(rendered)) + if err != nil { + t.Fatalf("WalkRefs() error = %v", err) + } + + if len(refs) == 0 { + t.Fatal("WalkRefs() returned no refs; the aliased names are no longer validated") + } + + link := network.NewLinkStatus(network.NamespaceName, "eth0") + link.TypedSpec().Alias = "uplink0" + + snapshot := applycheck.HostSnapshot{Links: snapshotLinkNames(slices.Values([]*network.LinkStatus{link}))} + + for _, finding := range applycheck.ValidateRefs(refs, snapshot) { + if finding.IsBlocker() { + t.Errorf("blocked an aliased link: ref=%+v reason=%s", finding.Ref, finding.Reason) + } + } +} From 4ec3b5ec91c453d2d519a1e257bee93a504d1640 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 11 Sep 2026 11:29:40 +0300 Subject: [PATCH 04/10] fix(commands): keep meta --insecure reachable on Talos v1.14 Talos v1.14 registers the meta command's --insecure on its local flag set rather than its persistent one. A local flag on a command that only hosts subcommands reaches nothing: cobra merges a parent's persistent flags into a child at parse time but not its local ones, and the parent itself takes no arguments. Both `meta write --insecure` and `meta --insecure write` stopped parsing, and with them the only way to write a META key over the maintenance service, which is what an operator does before a node has a machine config. Re-publish a wrapped container command's local flags as persistent ones, so they reach the subcommand that consumes them. The flag object is reused rather than redeclared, so it still writes to the variable upstream's RunE reads, and a flag that is already persistent is left alone, which makes this inert once the fix lands upstream. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin --- docs/manual-test-plan.md | 7 +- docs/operations/talosctl-commands.md | 2 - docs/reference/meta_delete.md | 20 ++-- docs/reference/meta_write.md | 20 ++-- pkg/commands/talosctl_wrapper.go | 42 +++++++++ pkg/commands/talosctl_wrapper_test.go | 130 +++++++++++++++++++++----- 6 files changed, 175 insertions(+), 46 deletions(-) diff --git a/docs/manual-test-plan.md b/docs/manual-test-plan.md index feb8fb03..c0e5ddd4 100644 --- a/docs/manual-test-plan.md +++ b/docs/manual-test-plan.md @@ -1341,16 +1341,17 @@ talm get metakey --nodes $NODE --endpoints $NODE Expected: table of META keys with their values. -### G1a. `--insecure` does not reach `meta` subcommands on Talos v1.14 +### G1a. `--insecure` reaches `meta` subcommands ```bash talm meta write --insecure 0x0a "test-value" --nodes $NODE --endpoints $NODE +talm meta write -i 0x0a "test-value" --nodes $NODE --endpoints $NODE talm meta --insecure write 0x0a "test-value" --nodes $NODE --endpoints $NODE ``` -Expected on v1.14: both forms fail with `unknown flag: --insecure`. Upstream registers the flag on `metaCmd.Flags()` rather than `PersistentFlags()`, so it reaches neither the subcommand (a local parent flag is not inherited) nor the parent (which takes no args), and `talosctl` behaves the same way. talm mirrors upstream instead of compensating, so META writes against a node in maintenance mode need a Talos-side fix. +Expected: all three reach the node over the maintenance service. Run this against a node in maintenance mode, before it has a machine config, which is the case the flag exists for. -Tracked upstream as [siderolabs/talos#14346](https://github.com/siderolabs/talos/issues/14346). Re-run this case after every Talos bump: once upstream restores the flag, the first form starts working again and this case flips to pinning that. +Talos v1.14 registers the flag on `metaCmd.Flags()` rather than `PersistentFlags()`, where it reaches neither the subcommands nor the parent, so plain `talosctl` rejects all three spellings. talm re-publishes container-command flags as persistent to keep this path working ([siderolabs/talos#14346](https://github.com/siderolabs/talos/issues/14346), fixed by [#14347](https://github.com/siderolabs/talos/pull/14347), merged to `main` and not yet in a v1.14 release). Re-run after every Talos bump: once the fix ships in the release talm tracks, the wrapper step is redundant and should be removed. ### G2. Write a test key diff --git a/docs/operations/talosctl-commands.md b/docs/operations/talosctl-commands.md index fd840ff8..445beee6 100644 --- a/docs/operations/talosctl-commands.md +++ b/docs/operations/talosctl-commands.md @@ -32,7 +32,6 @@ Talos v1.14 registers `meta`'s `--insecure` on that command's local flag set ins That is the only way to write a META key over the maintenance service, which is what you do before a node has a machine config, so talm re-publishes such flags as persistent on its wrapper. `talm meta write --insecure` and the `-i` shorthand keep working, and `--cert-fingerprint` travels with them. Reported upstream as [siderolabs/talos#14346](https://github.com/siderolabs/talos/issues/14346) and fixed by [siderolabs/talos#14347](https://github.com/siderolabs/talos/pull/14347), which is merged to `main` but not backported to the v1.14 branch. Once the fix ships in a Talos release talm depends on, the wrapper step becomes redundant and is dropped. - ## `talm apply` — `--mode=reboot` is gone on Talos v1.14 Upstream stopped registering `reboot` among the values of the apply mode flag in v1.14, so `talm apply --mode=reboot` fails with `invalid argument "reboot" for "-m, --mode" flag`. Use `--mode=auto`, which the node promotes to a reboot when the change requires one. Shell completion reads the accepted values back from the flag, so it no longer suggests `reboot` either. @@ -44,7 +43,6 @@ Upstream stopped registering `--insecure` on `upgrade` in v1.14 (it had been dep ## `talm reset` — `--insecure` is gone on Talos v1.14 Upstream dropped `--insecure` from `reset` in v1.14, so `talm reset --insecure` fails with `unknown flag: --insecure`. Resetting a node that has no valid configuration is done from the maintenance side instead: boot the node into a maintenance image and apply a fresh config, rather than resetting over an unauthenticated connection. - ## `talm reset` — META-preserving default `talm reset` diverges from upstream `talosctl reset` on one default. Upstream defaults to `--wipe-mode=all`, which wipes the Talos META partition along with STATE and EPHEMERAL — the node cannot self-recover and comes up in maintenance mode requiring a full re-apply. Talm instead populates `--system-labels-to-wipe=STATE,EPHEMERAL` when neither `--wipe-mode` nor `--system-labels-to-wipe` was passed, which preserves META so the node rejoins the cluster from its META-stored bootstrap config on the next boot. diff --git a/docs/reference/meta_delete.md b/docs/reference/meta_delete.md index da4eef54..394257c3 100644 --- a/docs/reference/meta_delete.md +++ b/docs/reference/meta_delete.md @@ -16,15 +16,17 @@ talm meta delete key [flags] ## Options inherited from parent commands ``` - --cluster string Cluster to connect to if a proxy endpoint is used. - --context string Context to be used in command - -e, --endpoints strings override default endpoints in Talos configuration - --nodes strings target the specified nodes - --root string root directory of the project (default ".") - --skip-verify skip TLS certificate verification (keeps client authentication) - --strict-charts fail if the project's vendored charts/talm/ or pinned preset baseline differs from the talm binary (run talm init --update --preset to re-sync) - --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. - --version Print the version number of the application + --cert-fingerprint strings list of server certificate fingerprints to accept (defaults to no check, only used with --insecure flag) + --cluster string Cluster to connect to if a proxy endpoint is used. + --context string Context to be used in command + -e, --endpoints strings override default endpoints in Talos configuration + -i, --insecure use the insecure (encrypted with no auth) maintenance service + --nodes strings target the specified nodes + --root string root directory of the project (default ".") + --skip-verify skip TLS certificate verification (keeps client authentication) + --strict-charts fail if the project's vendored charts/talm/ or pinned preset baseline differs from the talm binary (run talm init --update --preset to re-sync) + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. + --version Print the version number of the application ``` ## SEE ALSO diff --git a/docs/reference/meta_write.md b/docs/reference/meta_write.md index a314e7ae..05d6f149 100644 --- a/docs/reference/meta_write.md +++ b/docs/reference/meta_write.md @@ -16,15 +16,17 @@ talm meta write key value [flags] ## Options inherited from parent commands ``` - --cluster string Cluster to connect to if a proxy endpoint is used. - --context string Context to be used in command - -e, --endpoints strings override default endpoints in Talos configuration - --nodes strings target the specified nodes - --root string root directory of the project (default ".") - --skip-verify skip TLS certificate verification (keeps client authentication) - --strict-charts fail if the project's vendored charts/talm/ or pinned preset baseline differs from the talm binary (run talm init --update --preset to re-sync) - --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. - --version Print the version number of the application + --cert-fingerprint strings list of server certificate fingerprints to accept (defaults to no check, only used with --insecure flag) + --cluster string Cluster to connect to if a proxy endpoint is used. + --context string Context to be used in command + -e, --endpoints strings override default endpoints in Talos configuration + -i, --insecure use the insecure (encrypted with no auth) maintenance service + --nodes strings target the specified nodes + --root string root directory of the project (default ".") + --skip-verify skip TLS certificate verification (keeps client authentication) + --strict-charts fail if the project's vendored charts/talm/ or pinned preset baseline differs from the talm binary (run talm init --update --preset to re-sync) + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. + --version Print the version number of the application ``` ## SEE ALSO diff --git a/pkg/commands/talosctl_wrapper.go b/pkg/commands/talosctl_wrapper.go index a3f7e95b..d5fd81ba 100644 --- a/pkg/commands/talosctl_wrapper.go +++ b/pkg/commands/talosctl_wrapper.go @@ -141,6 +141,47 @@ func propagatePersistentFlags(cmd, wrappedCmd *cobra.Command) { }) } +// republishContainerFlags re-registers a container command's local flags as +// persistent ones on the wrapper. +// +// A local flag on a command that only hosts subcommands cannot be used at all: +// cobra merges a parent's persistent flags into a child at parse time but not +// its local ones, and a container command takes no arguments of its own. Talos +// v1.14 registers `meta`'s --insecure that way, which is what made +// `talosctl meta write --insecure` stop parsing (siderolabs/talos#14346). The +// flag object is reused rather than redeclared, so it still writes to the +// upstream variable the command's RunE reads. +// +// TODO: remove once siderolabs/talos#14347 ships in a Talos release talm +// depends on. It is merged upstream but sits on main, with no backport to the +// v1.14 branch, so v1.14.x still needs this. A flag that is already persistent +// is skipped, so the fix arriving turns this into a no-op, not a conflict. +// +//nolint:godox // deliberate removal marker tied to a specific upstream PR. +func republishContainerFlags(cmd, wrappedCmd *cobra.Command) { + if !cmd.HasSubCommands() { + return + } + + cmd.Flags().VisitAll(func(flag *pflag.Flag) { + if _, shadowed := rootShadowedPersistentFlags[flag.Name]; shadowed { + return + } + + if wrappedCmd.PersistentFlags().Lookup(flag.Name) != nil { + return + } + + if flag.Shorthand == "f" { + wrappedCmd.PersistentFlags().AddFlag(renameFlagShorthand(flag, "F")) + + return + } + + wrappedCmd.PersistentFlags().AddFlag(flag) + }) +} + // warnSkipVerifyUnsupported writes a warning to w when --skip-verify is set for // a wrapped talosctl passthrough command. Those commands run upstream RunE code // that builds its own client through upstream global.Args, which has no @@ -211,6 +252,7 @@ func wrapTalosCommand(cmd *cobra.Command, cmdName string) *cobra.Command { // "f". Today no such upstream flag exists, but the cost is one // branch and the surface stays uniform with the local-flag loop. propagatePersistentFlags(cmd, wrappedCmd) + republishContainerFlags(cmd, wrappedCmd) // Add --file flag only if it doesn't already exist in the original command var configFiles []string diff --git a/pkg/commands/talosctl_wrapper_test.go b/pkg/commands/talosctl_wrapper_test.go index 5a98c77a..29284f2c 100644 --- a/pkg/commands/talosctl_wrapper_test.go +++ b/pkg/commands/talosctl_wrapper_test.go @@ -632,19 +632,17 @@ func TestWrapTalosCommand_RealCrashdumpPopulatesNodesFromControlPlane(t *testing } } -// TestWrapTalosCommand_RealMetaPropagatesInsecure pins that the wrapper copies -// the upstream meta command's --insecure across, long form and -i shorthand -// both, so a future regression that copies the flag but drops the shorthand -// attribute is caught. +// TestWrapTalosCommand_RealMetaReachesSubcommandsWithInsecure pins that +// --insecure reaches `meta write`, long form and -i shorthand both. // -// It asserts on `meta`, not on `meta write`. Talos v1.14.0 moved --insecure -// from metaCmd.PersistentFlags() to metaCmd.Flags() when it introduced -// global.InsecureFlags, and a local flag does not reach a subcommand, so -// `talosctl meta write --insecure` stopped parsing upstream as well. talm -// mirrors upstream here rather than papering over it; compensating would mean -// talm accepting a flag talosctl rejects. Reported as -// https://github.com/siderolabs/talos/issues/14346. -func TestWrapTalosCommand_RealMetaPropagatesInsecure(t *testing.T) { +// Talos v1.14.0 moved the flag from metaCmd.PersistentFlags() to metaCmd.Flags() +// when it introduced global.InsecureFlags. A local flag on a container command +// reaches neither its subcommands nor the command itself, which took away the +// only way to write a META key over the maintenance service +// (https://github.com/siderolabs/talos/issues/14346). The wrapper re-publishes +// such flags as persistent, so writing META on a node in maintenance mode keeps +// working while the upstream fix is in flight. +func TestWrapTalosCommand_RealMetaReachesSubcommandsWithInsecure(t *testing.T) { var metaCmd *cobra.Command for _, cmd := range taloscommands.Commands { @@ -661,23 +659,109 @@ func TestWrapTalosCommand_RealMetaPropagatesInsecure(t *testing.T) { wrapped := wrapTalosCommand(metaCmd, "meta") - // Long form. - if err := wrapped.ParseFlags([]string{"--insecure"}); err != nil { - t.Fatalf("ParseFlags --insecure on wrapped meta: %v", err) + writeCmd, _, err := wrapped.Find([]string{"write"}) + if err != nil { + t.Fatalf("Find write under wrapped meta: %v", err) + } + + // Long form on the subcommand, which is where operators put it. + if err := writeCmd.ParseFlags([]string{"--insecure"}); err != nil { + t.Fatalf("ParseFlags --insecure on wrapped meta write: %v", err) } - if wrapped.Flags().Lookup("insecure") == nil { - t.Fatal("wrapped meta must carry --insecure across from the upstream command") + if writeCmd.Flags().Lookup("insecure") == nil { + t.Fatal("wrapped meta write must inherit --insecure from the wrapper") } // Short form. Re-parse to exercise the -i alias path. - if err := wrapped.ParseFlags([]string{"-i"}); err != nil { - t.Errorf("ParseFlags -i on wrapped meta: %v — shorthand attribute lost during copy?", err) + if err := writeCmd.ParseFlags([]string{"-i"}); err != nil { + t.Errorf("ParseFlags -i on wrapped meta write: %v — shorthand attribute lost during copy?", err) } - // The subcommands still exist and still do not see the parent's local flag, - // exactly as upstream leaves them. - if _, _, err := wrapped.Find([]string{"write"}); err != nil { - t.Fatalf("Find write under wrapped meta: %v", err) + // The fingerprint flag travels with it; --insecure alone would drop the + // pinning that makes the unauthenticated connection safe to use. + if writeCmd.Flags().Lookup("cert-fingerprint") == nil { + t.Error("wrapped meta write must inherit --cert-fingerprint alongside --insecure") + } +} + +// TestRepublishContainerFlags_SkipsShadowedFlags pins that a container +// command's local flag is dropped when talm's root already owns that name. +// Re-publishing it as persistent would shadow the root flag for the whole +// subtree, which is the collision rootShadowedPersistentFlags exists to avoid. +func TestRepublishContainerFlags_SkipsShadowedFlags(t *testing.T) { + upstream := &cobra.Command{Use: "upstream"} + upstream.Flags().String(talosconfigName, "", "collides with talm's root flag") + upstream.AddCommand(&cobra.Command{Use: "child"}) + + wrapped := &cobra.Command{Use: "wrapped"} + + republishContainerFlags(upstream, wrapped) + + if got := wrapped.PersistentFlags().Lookup(talosconfigName); got != nil { + t.Errorf("--%s was re-published despite being owned by talm's root command", talosconfigName) + } +} + +// TestRepublishContainerFlags_LeavesPersistentFlagsAlone pins the branch the +// removal plan rests on: a flag upstream already registered as persistent is +// left untouched. When siderolabs/talos#14347 lands, --insecure arrives +// persistent and this pass has to become a no-op instead of a second +// registration. +func TestRepublishContainerFlags_LeavesPersistentFlagsAlone(t *testing.T) { + upstream := &cobra.Command{Use: "upstream"} + upstream.PersistentFlags().Bool("insecure", false, "already persistent upstream") + upstream.AddCommand(&cobra.Command{Use: "child"}) + + wrapped := &cobra.Command{Use: "wrapped"} + + propagatePersistentFlags(upstream, wrapped) + + before := wrapped.PersistentFlags().Lookup("insecure") + if before == nil { + t.Fatal("the persistent pass must carry --insecure across") + } + + republishContainerFlags(upstream, wrapped) + + if after := wrapped.PersistentFlags().Lookup("insecure"); after != before { + t.Error("--insecure was re-registered; the pass must leave an already-persistent flag as it is") + } +} + +// TestRepublishContainerFlags_RenamesShorthandF pins the same defensive rename +// propagatePersistentFlags does: shorthand `f` belongs to talm's own --file, so +// a container command's local `-f` must arrive as `-F`. +func TestRepublishContainerFlags_RenamesShorthandF(t *testing.T) { + upstream := &cobra.Command{Use: "upstream"} + upstream.Flags().StringP("foo", "f", "default", "shadow shorthand f") + upstream.AddCommand(&cobra.Command{Use: "child"}) + + wrapped := &cobra.Command{Use: "wrapped"} + + republishContainerFlags(upstream, wrapped) + + got := wrapped.PersistentFlags().Lookup("foo") + if got == nil { + t.Fatal("re-publishing must register --foo on wrappedCmd.PersistentFlags(); got nil") + } + + if got.Shorthand != "F" { + t.Errorf("shorthand must be renamed from 'f' to 'F' to avoid colliding with talm's --file; got %q", got.Shorthand) + } +} + +// A command without subcommands keeps its local flags local: re-publishing them +// would widen their scope for no reason, since nothing inherits from it. +func TestRepublishContainerFlags_IgnoresLeafCommands(t *testing.T) { + upstream := &cobra.Command{Use: "upstream"} + upstream.Flags().Bool("insecure", false, "local to a leaf") + + wrapped := &cobra.Command{Use: "wrapped"} + + republishContainerFlags(upstream, wrapped) + + if wrapped.PersistentFlags().Lookup("insecure") != nil { + t.Error("a leaf command's local flag must not become persistent on the wrapper") } } From aaab35014b6ae627d3aa462814e0c613df73b24a Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 11 Sep 2026 11:58:26 +0300 Subject: [PATCH 05/10] fix(engine): keep every document of a rendered config The render decoded the serialized config into a single YAML node, which keeps only the first document of the stream. A Talos config is multi-document, and from the contract where Kubernetes settings moved out of v1alpha1 the generated bundle emits 28 of them: the certificate authorities, the service-account key, the kubelet, the control-plane settings, the volume and security profiles. All but the first were dropped, and the result still validates, so a full render produced a config missing the material a node needs to join. Decode the whole stream and re-emit every document in order. The v1alpha1 document keeps being the one that carries comments from the patches and the component-image strip; the typed documents pass through untouched. The non-full render returns a patch rather than a config, so it is not supposed to carry every document. It was still wrong on the same contract: the block that blanks cluster.clusterName and controlPlane.endpoint to force them into the diff was writing keys the serialized config no longer declares, and every node file came out carrying two delete directives against nothing. They were stripped again at apply, so nothing broke, but the node file should not have said it. Blank those fields only while they are still v1alpha1 ones. Signed-off-by: Aleksei Sviridkin Assisted-by: LLM --- docs/operations/talosctl-commands.md | 1 + go.mod | 1 - pkg/engine/contract_multidoc_render_test.go | 289 ++++++++++++++++++++ pkg/engine/engine.go | 146 +++++++++- pkg/engine/talos_helpers.go | 6 + 5 files changed, 428 insertions(+), 15 deletions(-) create mode 100644 pkg/engine/contract_multidoc_render_test.go diff --git a/docs/operations/talosctl-commands.md b/docs/operations/talosctl-commands.md index 445beee6..61e56aac 100644 --- a/docs/operations/talosctl-commands.md +++ b/docs/operations/talosctl-commands.md @@ -43,6 +43,7 @@ Upstream stopped registering `--insecure` on `upgrade` in v1.14 (it had been dep ## `talm reset` — `--insecure` is gone on Talos v1.14 Upstream dropped `--insecure` from `reset` in v1.14, so `talm reset --insecure` fails with `unknown flag: --insecure`. Resetting a node that has no valid configuration is done from the maintenance side instead: boot the node into a maintenance image and apply a fresh config, rather than resetting over an unauthenticated connection. + ## `talm reset` — META-preserving default `talm reset` diverges from upstream `talosctl reset` on one default. Upstream defaults to `--wipe-mode=all`, which wipes the Talos META partition along with STATE and EPHEMERAL — the node cannot self-recover and comes up in maintenance mode requiring a full re-apply. Talm instead populates `--system-labels-to-wipe=STATE,EPHEMERAL` when neither `--wipe-mode` nor `--system-labels-to-wipe` was passed, which preserves META so the node rejoins the cluster from its META-stored bootstrap config on the next boot. diff --git a/go.mod b/go.mod index 17bed7de..42ef51d9 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,6 @@ module github.com/cozystack/talm go 1.27.1 -// Kubernetes dependencies sharing the same version. require ( filippo.io/age v1.3.1 github.com/BurntSushi/toml v1.6.0 diff --git a/pkg/engine/contract_multidoc_render_test.go b/pkg/engine/contract_multidoc_render_test.go new file mode 100644 index 00000000..4243be2f --- /dev/null +++ b/pkg/engine/contract_multidoc_render_test.go @@ -0,0 +1,289 @@ +package engine + +import ( + "strings" + "testing" + + "github.com/siderolabs/talos/pkg/machinery/config/configloader" + "github.com/siderolabs/talos/pkg/machinery/config/encoder" + "github.com/siderolabs/talos/pkg/machinery/config/machine" +) + +// countYAMLDocuments counts non-empty documents in a YAML stream. +func countYAMLDocuments(t *testing.T, data []byte) int { + t.Helper() + + docs, err := decodeYAMLDocuments(data) + if err != nil { + t.Fatalf("decodeYAMLDocuments: %v", err) + } + + return len(docs) +} + +// A full render has to carry every document the bundle produced. From the +// contract where Talos moved the certificate authorities, the service-account +// key, the kubelet and the Kubernetes control-plane settings into documents of +// their own, keeping only the first one silently strips the config down to +// machine and cluster — and machinery validates the remains without complaint. +func TestContract_FullRenderKeepsEveryBundleDocument(t *testing.T) { + for _, talosVersion := range []string{"", "v1.12", "v1.14"} { + t.Run("talosVersion="+talosVersion, func(t *testing.T) { + opts := Options{KubernetesVersion: "v1.34.3", TalosVersion: talosVersion, Full: true} + + configBundle, err := InitializeConfigBundle(opts) + if err != nil { + t.Fatalf("InitializeConfigBundle: %v", err) + } + + fromBundle, err := configBundle.Serialize(encoder.CommentsDisabled, machine.TypeControlPlane) + if err != nil { + t.Fatalf("Serialize: %v", err) + } + + out, err := applyPatchesAndRenderConfig(opts, []string{"machine:\n type: controlplane\n"}) + if err != nil { + t.Fatalf("applyPatchesAndRenderConfig: %v", err) + } + + want := countYAMLDocuments(t, fromBundle) + if got := countYAMLDocuments(t, out); got != want { + t.Errorf("render produced %d documents, bundle has %d — documents are being dropped", got, want) + } + }) + } +} + +// The certificate authorities and the service-account key are what a node needs +// to join; losing them produces a config that still validates. +func TestContract_FullRenderKeepsSecretsOnMultidocContract(t *testing.T) { + out, err := applyPatchesAndRenderConfig( + Options{KubernetesVersion: "v1.34.3", TalosVersion: "v1.14", Full: true}, + []string{"machine:\n type: controlplane\n"}) + if err != nil { + t.Fatalf("applyPatchesAndRenderConfig: %v", err) + } + + for _, kind := range []string{"KubeAPIServerCAConfig", "KubeServiceAccountConfig", "KubeAggregatorCAConfig"} { + if !strings.Contains(string(out), "kind: "+kind) { + t.Errorf("render dropped the %s document", kind) + } + } +} + +// From the contract where Talos moved Kubernetes settings into their own +// documents, the bundle emits KubeAPIServerConfig and its siblings with the +// images already pinned to kubernetesVersion, and those documents survive the +// render. Writing the v1alpha1 fields as well would be rejected by machinery, +// which refuses a config carrying both shapes. +func TestContract_RenderLeavesV1Alpha1ComponentsAloneOnMultidocContract(t *testing.T) { + patch := `machine: + type: controlplane +` + + out, err := applyPatchesAndRenderConfig( + Options{KubernetesVersion: "v1.34.3", TalosVersion: "v1.14", Full: true}, + []string{patch}) + if err != nil { + t.Fatalf("applyPatchesAndRenderConfig: %v", err) + } + + rendered := string(out) + + if !strings.Contains(rendered, "kind: KubeAPIServerConfig") { + t.Fatalf("expected a KubeAPIServerConfig document in the render\n--- rendered ---\n%s", rendered) + } + + for _, conflicting := range []string{"\n apiServer:", "\n controllerManager:", "\n proxy:", "\n scheduler:"} { + if strings.Contains(rendered, conflicting) { + t.Errorf("v1alpha1 %q written next to the typed documents; machinery rejects a config with both\n--- rendered ---\n%s", strings.TrimSpace(conflicting), rendered) + } + } + + // The images still have to be pinned to kubernetesVersion, just by the bundle. + if !strings.Contains(rendered, "registry.k8s.io/kube-apiserver:v1.34.3") { + t.Errorf("typed documents do not carry the cluster's Kubernetes version\n--- rendered ---\n%s", rendered) + } +} + +// An apply sends the serialized bundle, not the node file. Both apply paths go +// through here: the template path renders with Full set, the direct-patch path +// serializes the bundle straight. Either way the node must receive the documents +// it needs to join, which the node file itself does not carry. +func TestContract_ApplyPathCarriesEveryDocument(t *testing.T) { + opts := Options{KubernetesVersion: "v1.34.3", TalosVersion: "v1.14"} + patch := `machine: + type: controlplane + install: + disk: /dev/sda +cluster: + controlPlane: + endpoint: https://192.0.2.4:6443 +` + + configBundle, machineType, err := FullConfigProcess(opts, []string{patch}) + if err != nil { + t.Fatalf("FullConfigProcess: %v", err) + } + + out, err := SerializeConfiguration(configBundle, machineType) + if err != nil { + t.Fatalf("SerializeConfiguration: %v", err) + } + + for _, kind := range []string{"KubeAPIServerCAConfig", "KubeServiceAccountConfig"} { + if !strings.Contains(string(out), "kind: "+kind) { + t.Errorf("the config an apply sends is missing the %s document", kind) + } + } +} + +// A chart emits typed documents of its own, and from contract v1.12 the bundle +// emits some of the same kinds. Talos rejects a config carrying two documents +// with one identity, so the chart's document has to replace the bundle's rather +// than join it — and the render has to stay loadable. +func TestContract_ChartDocumentReplacesBundleDocument(t *testing.T) { + patch := `machine: + type: controlplane + install: + disk: /dev/sda +cluster: + controlPlane: + endpoint: https://192.0.2.4:6443 +--- +apiVersion: v1alpha1 +kind: HostnameConfig +hostname: node0 +` + + for _, talosVersion := range []string{"v1.12", "v1.13"} { + t.Run("talosVersion="+talosVersion, func(t *testing.T) { + out, err := applyPatchesAndRenderConfig( + Options{KubernetesVersion: "v1.34.3", TalosVersion: talosVersion, Full: true}, + []string{patch}) + if err != nil { + t.Fatalf("applyPatchesAndRenderConfig: %v", err) + } + + if got := strings.Count(string(out), "kind: HostnameConfig"); got != 1 { + t.Errorf("render carries %d HostnameConfig documents, want 1\n--- rendered ---\n%s", got, out) + } + + if !strings.Contains(string(out), "hostname: node0") { + t.Errorf("the chart's hostname was dropped in favour of the bundle's\n--- rendered ---\n%s", out) + } + + if _, err := configloader.NewFromBytes(out); err != nil { + t.Errorf("Talos refuses the rendered config: %v", err) + } + }) + } +} + +// An extra document with neither apiVersion nor kind shares the identity +// sentinel with the v1alpha1 document itself. Treating that as a match would +// let a stray `---\nfoo: bar` in a chart delete machine, cluster, the tokens +// and the CAs, and the render would report nothing. +func TestContract_UntypedExtraDocumentKeepsMachineConfig(t *testing.T) { + patch := `machine: + type: controlplane + install: + disk: /dev/sda +cluster: + controlPlane: + endpoint: https://192.0.2.4:6443 +--- +someChartOutput: true +` + + for _, talosVersion := range []string{"v1.11", "v1.12", "v1.13"} { + t.Run("talosVersion="+talosVersion, func(t *testing.T) { + out, err := applyPatchesAndRenderConfig( + Options{KubernetesVersion: "v1.34.3", TalosVersion: talosVersion, Full: true}, + []string{patch}) + if err != nil { + t.Fatalf("applyPatchesAndRenderConfig: %v", err) + } + + rendered := string(out) + + if !strings.Contains(rendered, "machine:") || !strings.Contains(rendered, "cluster:") { + t.Errorf("the v1alpha1 document was dropped by an untyped extra document\n--- rendered ---\n%s", rendered) + } + + if !strings.Contains(rendered, "someChartOutput") { + t.Errorf("the extra document itself went missing\n--- rendered ---\n%s", rendered) + } + }) + } +} + +// A non-full render returns a patch, so it does not carry every bundle +// document — but it must not manufacture directives against fields the +// contract moved out of v1alpha1, and the operator's own typed documents have +// to survive it. Both halves are what makes the node file reproduce the +// cluster on the next apply. +func TestContract_NonFullRenderOnMultidocContract(t *testing.T) { + typed := `apiVersion: v1alpha1 +kind: KubeClusterConfig +clusterName: prod +endpoint: https://192.0.2.10:6443 +` + + out, err := applyPatchesAndRenderConfig( + Options{KubernetesVersion: "v1.34.3", TalosVersion: "v1.14"}, + []string{"machine:\n type: controlplane\n", typed}) + if err != nil { + t.Fatalf("applyPatchesAndRenderConfig: %v", err) + } + + rendered := string(out) + + if !strings.Contains(rendered, "kind: KubeClusterConfig") || !strings.Contains(rendered, "clusterName: prod") { + t.Errorf("render dropped the operator's KubeClusterConfig:\n%s", rendered) + } + + // cluster.clusterName and cluster.controlPlane.endpoint live in + // KubeClusterConfig from this contract on, so nothing should be deleting + // them from a v1alpha1 document that no longer declares them. + if strings.Contains(rendered, "$patch: delete") { + t.Errorf("render emitted a delete directive against fields the contract moved:\n%s", rendered) + } +} + +// The non-full render diffs the serialized bundle against itself through +// yamltools.DiffYAMLs, which compares the first document of each side and +// ignores the rest. That is only correct while the first document is the +// v1alpha1 one on every contract. Nothing in machinery promises the order, and +// the cost of it changing grew with this bump: the bundle went from two +// documents to twenty-eight, so a reordering would silently diff v1alpha1 +// against a typed document instead of failing. +func TestContract_BundleSerializesV1Alpha1First(t *testing.T) { + for _, talosVersion := range []string{"", "v1.12", "v1.13", "v1.14"} { + t.Run("talosVersion="+talosVersion, func(t *testing.T) { + configBundle, err := InitializeConfigBundle( + Options{KubernetesVersion: "v1.34.3", TalosVersion: talosVersion}) + if err != nil { + t.Fatalf("InitializeConfigBundle: %v", err) + } + + serialized, err := configBundle.Serialize(encoder.CommentsDisabled, machine.TypeControlPlane) + if err != nil { + t.Fatalf("Serialize: %v", err) + } + + docs, err := decodeYAMLDocuments(serialized) + if err != nil { + t.Fatalf("decodeYAMLDocuments: %v", err) + } + + if len(docs) == 0 { + t.Fatal("bundle serialized to no documents") + } + + if id := documentIdentityFromNode(docs[0]); id != legacyRootIdentity { + t.Errorf("first serialized document is %q, want the v1alpha1 root: the non-full render diffs doc[0]", id) + } + }) + } +} diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 12b03556..a8956a94 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -1977,7 +1977,13 @@ func applyPatchesAndRenderConfig(opts Options, configPatches []string) ([]byte, mtype[cosiMetaKeyType] = "unknown" } - if cluster, ok := cfg["cluster"].(map[string]any); ok { + // Blanking a field here is what forces it into the diff, so the node + // file pins it instead of inheriting whatever the next render computes. + // It only works while the field is still a v1alpha1 one: from the + // contract that moved the cluster identity into KubeClusterConfig, + // blanking would invent a key the serialized config no longer has, and + // the diff would come out as a delete directive against nothing. + if cluster, ok := cfg["cluster"].(map[string]any); ok && !versionContract.MultidocKubernetesConfigSupported() { cluster["clusterName"] = "" controlPlane, ok := cluster["controlPlane"].(map[string]any) @@ -2015,40 +2021,152 @@ func applyPatchesAndRenderConfig(opts Options, configPatches []string) ([]byte, } } - var targetNode yaml.Node + buf, err := assembleTargetDocuments(target, talosPatches, extraDocs) + if err != nil { + return nil, err + } - err = yaml.Unmarshal(target, &targetNode) + return buf.Bytes(), nil +} + +// assembleTargetDocuments turns the serialized target into the bytes the render +// returns: every document preserved in order, with the operator's comments +// carried over onto the v1alpha1 one, followed by the chart's own documents. +// +// A chart document replaces the bundle's document of the same identity rather +// than joining it. From contract v1.12 the bundle emits typed documents the +// charts also emit (HostnameConfig is the common one), and Talos rejects a +// config carrying two documents with the same apiVersion/kind/name. +func assembleTargetDocuments(target []byte, talosPatches, extraDocs []string) (*bytes.Buffer, error) { + // Decode the whole stream: a Talos config is multi-document, and from the + // contract that moved Kubernetes settings out of v1alpha1 the generated + // bundle puts the certificate authorities, the service-account key, the + // kubelet and the control-plane settings in documents of their own. + // Unmarshaling into a single node keeps only the first one, which would drop + // everything after machine and cluster. + targetDocs, err := decodeYAMLDocuments(target) if err != nil { - return nil, errors.Wrap(err, "unmarshaling target config") + return nil, err + } + + if len(targetDocs) == 0 { + return nil, errors.New("rendered config is empty") } - // Copy comments from source configuration to the final output for _, configPatch := range talosPatches { var sourceNode yaml.Node - err = yaml.Unmarshal([]byte(configPatch), &sourceNode) - if err != nil { + if err := yaml.Unmarshal([]byte(configPatch), &sourceNode); err != nil { return nil, errors.Wrap(err, "unmarshaling source patch for comment propagation") } dstPaths := make(map[string]*yaml.Node) - yamltools.CopyComments(&sourceNode, &targetNode, "", dstPaths) - yamltools.ApplyComments(&targetNode, "", dstPaths) + yamltools.CopyComments(&sourceNode, targetDocs[0], "", dstPaths) + yamltools.ApplyComments(targetDocs[0], "", dstPaths) } - buf := &bytes.Buffer{} - if err := encodeYAMLNodeIndented(buf, &targetNode); err != nil { + extraIdentities, err := documentIdentities(extraDocs) + if err != nil { return nil, err } - // Append extra documents (like UserVolumeConfig) that are not part of Talos config + buf := &bytes.Buffer{} + written := 0 + + for _, doc := range targetDocs { + if len(doc.Content) > 0 { + if _, superseded := extraIdentities[documentIdentityFromNode(doc.Content[0])]; superseded { + continue + } + } + + if written > 0 { + buf.WriteString("---\n") + } + + if err := encodeYAMLNodeIndented(buf, doc); err != nil { + return nil, err + } + + written++ + } + + // Extra documents (UserVolumeConfig and the chart's typed documents) are + // emitted verbatim, so operator formatting survives the round-trip. for _, extraDoc := range extraDocs { - buf.WriteString("---\n") + if written > 0 { + buf.WriteString("---\n") + } + buf.WriteString(extraDoc) buf.WriteString("\n") + + written++ } - return buf.Bytes(), nil + return buf, nil +} + +// documentIdentities indexes raw YAML documents by apiVersion/kind/name. +func documentIdentities(docs []string) (map[string]struct{}, error) { + identities := make(map[string]struct{}, len(docs)) + + for _, doc := range docs { + var node yaml.Node + + if err := yaml.Unmarshal([]byte(doc), &node); err != nil { + return nil, errors.Wrap(err, "unmarshaling extra document") + } + + if len(node.Content) == 0 { + continue + } + + identity := documentIdentityFromNode(node.Content[0]) + + // legacyRootIdentity is the sentinel for a mapping with neither + // apiVersion nor kind, which is what the v1alpha1 document itself + // returns. Indexing it would make any untyped extra document supersede + // machine and cluster, silently emptying the config. + if identity == legacyRootIdentity { + continue + } + + identities[identity] = struct{}{} + } + + return identities, nil +} + +// decodeYAMLDocuments splits a YAML stream into one node per document, +// preserving order. Documents that hold nothing (a trailing separator, a +// comment-only chunk) are dropped rather than re-emitted as empty ones. +func decodeYAMLDocuments(data []byte) ([]*yaml.Node, error) { + dec := yaml.NewDecoder(bytes.NewReader(data)) + + var docs []*yaml.Node + + for { + var doc yaml.Node + + err := dec.Decode(&doc) + + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + return nil, errors.Wrap(err, "unmarshaling target config") + } + + if len(doc.Content) == 0 { + continue + } + + docs = append(docs, &doc) + } + + return docs, nil } // encodeYAMLNodeIndented writes node to w as 2-space-indented YAML diff --git a/pkg/engine/talos_helpers.go b/pkg/engine/talos_helpers.go index a35b0371..422a8db6 100644 --- a/pkg/engine/talos_helpers.go +++ b/pkg/engine/talos_helpers.go @@ -50,6 +50,12 @@ func failIfMultiNodes(ctx context.Context, command string) error { // forEachResource resolves a resource kind and runs callback for every resource // of that kind, on every node named in the context's outgoing metadata. // +// The per-node loop is not unit-tested: resolving the kind goes through +// client.ResolveResourceKind, which is a concrete method on the Talos client +// and cannot be substituted. walkNodeResources below carries the per-node body +// and is tested directly; the loop itself is covered by the multi-node lookup +// case in the manual test plan. +// // The kind is resolved once, against the first node: a resource definition is // cluster-wide, so which node answers does not matter. A per-node lookup failure // is handed to the callback rather than returned, which is what lets a caller From 35c1fad3719e0471c25fe3fd95c5fc42d81351ce Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 11 Sep 2026 14:01:56 +0300 Subject: [PATCH 06/10] fix(engine): report the version keys a render can no longer guess MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two keys used to have a working empty value and no longer do, and both failed quietly rather than loudly. An unset talosVersion means the Talos version talm was built against, which is still true. What changed under it is Talos: from v1.14 the Kubernetes settings the charts write as v1alpha1 fields (machine.kubelet, machine.nodeLabels) live in documents of their own, and machinery rejects a config carrying both shapes. An unpinned project therefore rendered a config the node refuses, with nothing said until the apply failed. The render now stops and names the fields and the way out. An unset kubernetesVersion used to mean "the node decides": machinery emitted no image field at all. v1.14 refuses to generate without a version, so the fallback that satisfies it would write this binary's Kubernetes version into the config instead — a version jump nobody asked for, invisible in the node file because the render's diff drops fields equal to the bundle default. On contracts where the node can still choose, the generated images are stripped again; on contracts where the typed documents require an image, the missing pin is reported. The generic preset gains the pins it now needs — cozystack already had them — and both errors carry a hint naming the key. A project created before this renders again once its Chart.yaml pins templateOptions.talosVersion and templateOptions.kubernetesVersion. Add the contract test that would have caught the first one: nothing in the suite loaded a rendered config back through machinery, so a render could stop being applicable while the whole suite stayed green. Signed-off-by: Aleksei Sviridkin Assisted-by: LLM --- README.md | 2 +- charts/generic/Chart.yaml | 9 +- docs/configuration/talos-versions.md | 40 +- docs/index.md | 2 +- docs/manual-test-plan.md | 15 + docs/operations/safety-gates.md | 2 +- docs/operations/talosctl-commands.md | 9 + docs/operations/upgrading.md | 20 + docs/reference/apply.md | 4 +- docs/reference/template.md | 2 +- go.mod | 1 + main.go | 4 - main_test.go | 29 + pkg/applycheck/refs_netaddr_test.go | 42 +- pkg/applycheck/validate_test.go | 8 +- pkg/commands/apply.go | 8 +- pkg/commands/contract_stdout_silence_test.go | 1 + pkg/commands/contract_template_test.go | 28 + pkg/commands/preflight_upgrade_verify_test.go | 9 +- pkg/commands/template.go | 3 +- pkg/engine/contract_multidoc_render_test.go | 7 +- pkg/engine/contract_render_test.go | 23 +- pkg/engine/contract_validate_render_test.go | 506 ++++++++++++++++++ pkg/engine/engine.go | 401 ++++++++++++-- pkg/engine/golden_test.go | 4 + pkg/engine/talos_helpers.go | 5 +- .../generic-controlplane-v113.golden.yaml | 49 ++ .../golden/generic-worker-v113.golden.yaml | 45 ++ 28 files changed, 1186 insertions(+), 92 deletions(-) create mode 100644 pkg/engine/contract_validate_render_test.go create mode 100644 pkg/engine/testdata/golden/generic-controlplane-v113.golden.yaml create mode 100644 pkg/engine/testdata/golden/generic-worker-v113.golden.yaml diff --git a/README.md b/README.md index 21445cf6..184e9f90 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Full documentation lives at **[talm.cozystack.io](https://talm.cozystack.io/)**. - [Initializing a project](https://talm.cozystack.io/getting-started/init/) — `--image`, `--cluster-endpoint`, `--root`. - [Node files](https://talm.cozystack.io/configuration/node-files/) — the modeline, discovery comments, per-node overrides. - [Endpoints and VIPs](https://talm.cozystack.io/configuration/endpoints-and-vips/) — `endpoint`, `floatingIP`, `vipLink`. -- [Talos versions and output format](https://talm.cozystack.io/configuration/talos-versions/) — the v1.12 multi-document split. +- [Talos versions and output format](https://talm.cozystack.io/configuration/talos-versions/) — which schema a render targets, and the version keys to pin. - [Templates and values](https://talm.cozystack.io/configuration/templates/) — `lookup`, `--set` vs `--set-string`. - [Encryption](https://talm.cozystack.io/configuration/encryption/) — age-encrypted secrets and user values. - [Applying with side-patches](https://talm.cozystack.io/operations/side-patches/) — the `-f` chain. diff --git a/charts/generic/Chart.yaml b/charts/generic/Chart.yaml index 3ebdf961..e00d02bb 100644 --- a/charts/generic/Chart.yaml +++ b/charts/generic/Chart.yaml @@ -12,9 +12,14 @@ templateOptions: fileValues: [] jsonValues: [] literalValues: [] - talosVersion: "" + # Pin both: an empty key either renders what the node picks (with a warning) + # or stops the render on newer contracts. The Kubernetes version is the + # default of the pinned Talos line, so a fresh project lands on what that + # line ships and a re-sync does not move the control plane. Replace it with + # the version your cluster runs. + talosVersion: "v1.13" withSecrets: "secrets.yaml" - kubernetesVersion: "" + kubernetesVersion: "v1.36.2" full: false applyOptions: preserve: false diff --git a/docs/configuration/talos-versions.md b/docs/configuration/talos-versions.md index 983a48e5..d975ef18 100644 --- a/docs/configuration/talos-versions.md +++ b/docs/configuration/talos-versions.md @@ -2,12 +2,46 @@ `templateOptions.talosVersion` in `Chart.yaml` (or `--talos-version` on the command line) selects which Talos config schema talm renders. It changes the shape of every node file. +## What an unset version means + +Leaving `templateOptions.talosVersion` empty means the version of Talos the `talm` binary was built against. It is not frozen at a release: upgrade talm, and an unpinned project follows it to whatever Talos that build carries. + +That is why the shipped presets pin it. The charts write Kubernetes settings as v1alpha1 fields (`machine.kubelet`, `machine.nodeLabels`), and from v1.14 Talos keeps each of those in a document of its own and rejects a config that carries both shapes. Rendering an unpinned project against v1.14 therefore produces a config the node refuses, with `kubelet config is already set in v1alpha1 config (.machine.kubelet)` and siblings. Network settings made the same move earlier, in v1.12. + +Rather than emit that config, the render stops. With both keys empty the Kubernetes version is reported first, since the bundle cannot be serialized without one; pin it and the contract conflict is reported next: + +```text +rendered config mixes v1alpha1 fields with the documents that superseded them: ... +hint: templateOptions.talosVersion is unset, so the render targets the Talos version talm was built from. + Pin it to v1.13 or lower in Chart.yaml; above that contract Talos keeps these settings in documents of their own, + which the charts do not emit. +``` + +A chart can also fail earlier than either guard: the cozystack preset deletes a default node label with `$patch: delete`, and on a contract where `machine.nodeLabels` has moved into `KubeNodeConfig` that path no longer exists, so patching stops with `failed to delete path ...: lookup failed` before any version check runs. The preset happens to pin `v1.12`, which predates that move and sidesteps it. + +Pin `templateOptions.talosVersion` in `Chart.yaml`. The rule below still holds — the contract must not be newer than the Talos running on the node — and until the charts emit the typed documents there is a second bound on top of it: keep the pin at or below `v1.13`. Nodes running v1.14 are served by a v1.13 contract, which is the supported direction; the reverse is not. Projects created from the shipped presets already carry a pin. + +## The Kubernetes version + +`templateOptions.kubernetesVersion` picks the Kubernetes release the generated config targets: the kubelet image and, on contracts that keep them in v1alpha1, the control-plane component images. + +Leaving it empty means the node keeps deciding, on contracts where it still can: the render emits no image fields at all, and each component uses the default of the Talos release it runs. The render says so on stderr, because that is a real choice and not a no-op — a node on a newer Talos release moves kubelet and the control plane with it, and the node file shows nothing either way. This is new behaviour, not a restored default: earlier talm substituted its own built-in Kubernetes version whenever the key was empty, so an unpinned project silently followed the binary. It no longer does. + +From v1.14 the contract takes that choice away. The Kubernetes settings live in documents of their own and those documents require an image, so an unset version is reported rather than rendered — pin the key. + +Pin it in `Chart.yaml` when you want to choose the version yourself, which is what both shipped presets do. A pinned version reaches every component, kubelet included, so they never split across releases. + +The presets do not agree on a value. Generic pins `v1.36.2`, the default of the Talos line it targets, so a fresh project lands on what that line ships and a re-sync does not move the control plane. Cozystack pins `v1.34.3` against an older Talos line. Either way the pin is a starting point: set the key to the version your own cluster runs rather than inheriting the preset's. + +A project created before those pins existed carries empty values for this key and for `talosVersion`. On the first render after upgrading talm it stops with an error naming the key to set; add both pins to its `Chart.yaml` and it renders again. + ## Which format each version gets Selected via `Chart.yaml` (`templateOptions.talosVersion`) or `--talos-version`: - **Talos < v1.12** — single YAML document with `machine.network` and `machine.registries` sections (the shape shown in [Node files](node-files.md)). -- **Talos >= v1.12** — multi-document format with separate typed documents instead of the deprecated monolithic fields. +- **Talos >= v1.12** — multi-document format with separate typed documents instead of the deprecated monolithic network and registry fields. +- **Talos >= v1.14** — a second round of the same move, this time for the Kubernetes settings: `machine.kubelet`, `machine.nodeLabels` and the control-plane components each get a document of their own, and a config carrying both shapes is rejected. The shipped charts still write the v1alpha1 fields, which is why the presets pin below this. ## Documents emitted in multi-doc mode @@ -39,6 +73,6 @@ Multi-NIC nodes therefore produce one document per NIC, not one document total. ## Version compatibility -!!! danger "The configured version must match the Talos actually running on the node" +!!! danger "The configured version must not be newer than the Talos running on the node" - 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. + This setting must not exceed 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. diff --git a/docs/index.md b/docs/index.md index a1c4fd1a..6dc1a2a3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -82,6 +82,6 @@ talm apply -f nodes/node1.yaml -i - [Initializing a project](getting-started/init.md) — `--image`, `--cluster-endpoint`, `--root`. - [Endpoints and VIPs](configuration/endpoints-and-vips.md) — `endpoint`, `floatingIP`, `vipLink`. -- [Talos versions and output format](configuration/talos-versions.md) — the v1.12 multi-document split. +- [Talos versions and output format](configuration/talos-versions.md) — which schema a render targets, and the version keys to pin. - [Applying with side-patches](operations/side-patches.md) — the `-f` chain. - [CLI reference](reference/index.md) — generated from the command tree. diff --git a/docs/manual-test-plan.md b/docs/manual-test-plan.md index c0e5ddd4..57edee60 100644 --- a/docs/manual-test-plan.md +++ b/docs/manual-test-plan.md @@ -48,6 +48,21 @@ Watch for: - `talm.key` written without the security-information banner. - `.gitignore` not updated. +### A1a. Version pins on a project created before them + +Run inside the A1 project (`--preset cozystack`, which pins `talosVersion: "v1.12"`). + +```bash +sed -i'' -e 's|talosVersion: .*|talosVersion: ""|' -e 's|kubernetesVersion: .*|kubernetesVersion: ""|' Chart.yaml +talm template --offline --full -t templates/controlplane.yaml +``` + +Expected: the render stops with `templateOptions.kubernetesVersion is not set` and a hint naming the key. + +Restore `talosVersion: "v1.12"` alone and re-run: the render succeeds and warns on stderr that no component images are pinned. Restore the preset's `kubernetesVersion: "v1.34.3"` too and re-run: no warning, and the output carries `kubelet`, `kube-apiserver`, `kube-controller-manager`, `kube-proxy` and `kube-scheduler` images on that version. + +`--full` matters here. A default render is a diff against the same bundle, so the images cancel out on both sides and never appear in a node file either way. + ### A2. `talm init` second run without `--force` ```bash diff --git a/docs/operations/safety-gates.md b/docs/operations/safety-gates.md index e0c65ade..8eeb68b5 100644 --- a/docs/operations/safety-gates.md +++ b/docs/operations/safety-gates.md @@ -11,7 +11,7 @@ ## 1. Declared-resource existence -Before sending the config to the node, the gate walks the rendered MachineConfig, extracts every reference to a host-side resource (network links from v1.12 multi-doc — `LinkConfig.name`, `BondConfig.links[]`, `VLANConfig.parent`, `BridgeConfig.links[]`, `VRFConfig.links[]`, `Layer2VIPConfig.link`, `HCloudVIPConfig.link`, `DHCPv4Config.name` / `DHCPv6Config.name` / `EthernetConfig.name`, and from v1.14 `BGPInstanceConfig.advertise[]` and `BGPInstanceConfig.neighbors[].link`; v1.11 legacy `machine.network.interfaces[].interface`; install disk via `machine.install.disk` literal or `machine.install.diskSelector`; `UserVolumeConfig.provisioning.diskSelector`), and verifies each against the node's COSI `LinkStatus`/`Disk` snapshots. A reference that doesn't resolve fails the apply with a `[blocker]` line listing the available names so the typo or migration miss is fixable from the values without re-running discovery. +Before sending the config to the node, the gate walks the rendered MachineConfig, extracts every reference to a host-side resource (network links from v1.12 multi-doc — `LinkConfig.name`, `BondConfig.links[]`, `VLANConfig.parent`, `BridgeConfig.links[]`, `Layer2VIPConfig.link`, `HCloudVIPConfig.link`, `DHCPv4Config.name` / `DHCPv6Config.name` / `EthernetConfig.name`; from v1.13 `VRFConfig.links[]`; from v1.14 `BGPInstanceConfig.advertise[]` and `BGPInstanceConfig.neighbors[].link`; v1.11 legacy `machine.network.interfaces[].interface`; install disk via `machine.install.disk` literal or `machine.install.diskSelector`; `UserVolumeConfig.provisioning.diskSelector`), and verifies each against the node's COSI `LinkStatus`/`Disk` snapshots. A reference that doesn't resolve fails the apply with a `[blocker]` line listing the available names so the typo or migration miss is fixable from the values without re-running discovery. Disk selectors must match at least one (non-readonly, non-CDROM, non-virtual) disk — zero matches block, multiple matches warn (install picks the first). diff --git a/docs/operations/talosctl-commands.md b/docs/operations/talosctl-commands.md index 61e56aac..ac406870 100644 --- a/docs/operations/talosctl-commands.md +++ b/docs/operations/talosctl-commands.md @@ -32,6 +32,7 @@ Talos v1.14 registers `meta`'s `--insecure` on that command's local flag set ins That is the only way to write a META key over the maintenance service, which is what you do before a node has a machine config, so talm re-publishes such flags as persistent on its wrapper. `talm meta write --insecure` and the `-i` shorthand keep working, and `--cert-fingerprint` travels with them. Reported upstream as [siderolabs/talos#14346](https://github.com/siderolabs/talos/issues/14346) and fixed by [siderolabs/talos#14347](https://github.com/siderolabs/talos/pull/14347), which is merged to `main` but not backported to the v1.14 branch. Once the fix ships in a Talos release talm depends on, the wrapper step becomes redundant and is dropped. + ## `talm apply` — `--mode=reboot` is gone on Talos v1.14 Upstream stopped registering `reboot` among the values of the apply mode flag in v1.14, so `talm apply --mode=reboot` fails with `invalid argument "reboot" for "-m, --mode" flag`. Use `--mode=auto`, which the node promotes to a reboot when the change requires one. Shell completion reads the accepted values back from the flag, so it no longer suggests `reboot` either. @@ -44,6 +45,14 @@ Upstream stopped registering `--insecure` on `upgrade` in v1.14 (it had been dep Upstream dropped `--insecure` from `reset` in v1.14, so `talm reset --insecure` fails with `unknown flag: --insecure`. Resetting a node that has no valid configuration is done from the maintenance side instead: boot the node into a maintenance image and apply a fresh config, rather than resetting over an unauthenticated connection. +## `talm support` — the bundle is encrypted by default on Talos v1.14 + +Upstream changed the default: `talm support` now encrypts the generated bundle with age, to a built-in list of Sidero Labs recipients. `--no-encryption` turns that off, `--encryption-recipients` sends it to recipients you choose, and `--encryption-no-default-recipients` keeps yours while dropping theirs. Worth knowing before collecting a bundle from a cluster whose contents you would rather not hand to a third party, encrypted or otherwise. + +## `talm containers`, `logs`, `restart`, `stats` — `-k` / `--kubernetes` is deprecated + +Upstream replaced the flag with `--namespace`, which takes `system`, `cri` or `taloscontainers`. The old spelling still works and prints `Flag --kubernetes has been deprecated, use --namespace cri instead`, so scripts keep running for now; move them over before the flag goes the way of the others on this page. + ## `talm reset` — META-preserving default `talm reset` diverges from upstream `talosctl reset` on one default. Upstream defaults to `--wipe-mode=all`, which wipes the Talos META partition along with STATE and EPHEMERAL — the node cannot self-recover and comes up in maintenance mode requiring a full re-apply. Talm instead populates `--system-labels-to-wipe=STATE,EPHEMERAL` when neither `--wipe-mode` nor `--system-labels-to-wipe` was passed, which preserves META so the node rejoins the cluster from its META-stored bootstrap config on the next boot. diff --git a/docs/operations/upgrading.md b/docs/operations/upgrading.md index f0c4a197..88150210 100644 --- a/docs/operations/upgrading.md +++ b/docs/operations/upgrading.md @@ -36,6 +36,26 @@ The remediation needs the preset name because `talm init --update` resolves the Teams that want this enforced can turn the warning into a hard error (exit 1): set `strictCharts: true` in `Chart.yaml` so the whole team and CI inherit it, or pass `--strict-charts` for a single run. Strict mode applies to every config-loading command, including read-only ones such as `talm get` — run `talm init --update --preset `, or drop the flag / unset `strictCharts`, to unblock. Strict mode also escalates a check that cannot run at all — an unreadable `charts/talm/` or a corrupted `.talm-preset.lock` — into the same hard error, where the default behaviour degrades it to a `WARN: could not check drift` line: an unverifiable baseline passing silently would defeat the enforcement. A *missing* baseline (no `charts/talm/`, no `.talm-preset.lock`) blocks under strict for the same reason — deleting the baseline must not be a quieter bypass than corrupting it — while staying silent without strict, so projects generated before baseline pinning are not nagged. The check stays silent for `dev`/source builds, whose embedded charts are a moving target the developer controls. +## Version keys a project must pin + +A project created before the shipped presets carried version pins has `templateOptions.talosVersion` and `templateOptions.kubernetesVersion` empty in its `Chart.yaml`. On a talm built against Talos v1.14 or newer, that combination stops the render rather than guessing: + +```text +templateOptions.kubernetesVersion is not set +``` + +The line carries a prefix naming the step that hit it, and there are three: `serializing original config bundle:` on a default render, `serializing patched config bundle:` on a `--full` render or an apply that goes through templates, and `serializing configuration:` on an apply of a plain patch file. Grep for the part above rather than the whole line. + +A project pinned at or below `v1.13` with an empty `kubernetesVersion` renders without error, but no longer pins component images: each node then uses the Kubernetes version of its own Talos release, and the render warns on stderr. + +Node files never carried those images — a default render diffs against the same bundle, so they cancel out — but the config an apply sends did, because an earlier talm substituted its own built-in version for an empty key. In the drift preview this shows up as five `-` lines for the kubelet and control-plane images, and after the apply those components follow the node's own Talos release. If that release is older than the talm that last wrote the config, it is a downgrade. If it is newer, the same mechanism quietly upgrades the cluster's Kubernetes — the more common direction after a Talos bump. Read the preview before applying, or pin the key. + +Pin both keys to what the cluster actually runs, and keep `talosVersion` at or below `v1.13` until the charts emit the typed Kubernetes documents. [Talos versions and output format](../configuration/talos-versions.md) explains what each key selects and why an unpinned value stopped being safe. + +The generic preset gained its pins in this release, so its embedded hash changed: an existing generic-preset project will report preset drift until it is re-synced. (The cozystack preset already carried both pins, so projects on it are unaffected.) That is the mechanism described below, and `talm init --update --preset generic` re-syncs both at once — but read this before running it. + +It rewrites `Chart.yaml` from the preset, keeping only `name` and `version`, so the preset's pins replace whatever the project used. The preset pins `kubernetesVersion: "v1.36.2"`, which is what a project running with an empty key was already rendering on the Talos v1.13 line — accepting the re-sync leaves the control plane where it was. That holds only while the two track each other; the moment the preset's pin and the version the project actually runs diverge, accepting the preset's value moves the control plane, and a move **down** is not something Kubernetes supports. Set the key to the version your cluster runs rather than keeping whatever the re-sync wrote, and copy any other settings back too. The command also refuses to run non-interactively without `--force`. + ## Preset drift The vendored library (`charts/talm/`) is not the whole story. `talm init` also copies the **preset** — the `templates/` that render your machine config (sysctls, etcd args, the cozystack opinions) — into the project, and those you are *expected* to edit. That makes content comparison the wrong tool: it would flag every legitimate customization. So the preset is tracked differently. At `init` (and `init --update`) time talm pins the hash of the preset **as shipped** into `.talm-preset.lock`: diff --git a/docs/reference/apply.md b/docs/reference/apply.md index 01769332..a56acf80 100644 --- a/docs/reference/apply.md +++ b/docs/reference/apply.md @@ -33,14 +33,14 @@ talm apply [flags] ## Options ``` - --cert-fingerprint strings list of server certificate fingeprints to accept (defaults to no check) + --cert-fingerprint strings list of server certificate fingerprints to accept (defaults to no check) --debug show only rendered patches --dry-run check how the config change will be applied in dry-run mode -f, --file .yaml node config files / patches (.yaml / `.yml`; shell completion narrows to these extensions). First -f is the modelined anchor (must live under a `talm init`'d project root); subsequent -f files are side-patches stacked onto the anchor's rendered config and may live anywhere. --force will overwrite existing files -h, --help help for apply -i, --insecure apply using the insecure (encrypted with no auth) maintenance service - --kubernetes-version string desired kubernetes version to run (default "1.37.0") + --kubernetes-version string desired kubernetes version to run; defaults to templateOptions.kubernetesVersion from Chart.yaml -m, --mode auto, no-reboot, staged, try apply config mode (default auto) --set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2). Values that parse as an integer or a boolean are converted to that type; use --set-string to keep them as strings. --set-file stringArray set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) diff --git a/docs/reference/template.md b/docs/reference/template.md index bd7e5328..215df670 100644 --- a/docs/reference/template.md +++ b/docs/reference/template.md @@ -27,7 +27,7 @@ talm template [flags] -h, --help help for template -I, --in-place re-template and update generated files in place (overwrite them) -i, --insecure template using the insecure (encrypted with no auth) maintenance service - --kubernetes-version string desired kubernetes version to run (default "1.37.0") + --kubernetes-version string desired kubernetes version to run; defaults to templateOptions.kubernetesVersion from Chart.yaml --offline disable gathering information and lookup functions --set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2). Values that parse as an integer or a boolean are converted to that type; use --set-string to keep them as strings. --set-file stringArray set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) diff --git a/go.mod b/go.mod index 42ef51d9..8fa62556 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v4 v4.2.3 + // k8s.io modules move in lockstep; bump them together. k8s.io/api v0.37.0 k8s.io/apimachinery v0.37.0 k8s.io/client-go v0.37.0 diff --git a/main.go b/main.go index 27bee52e..b54d8f42 100644 --- a/main.go +++ b/main.go @@ -448,10 +448,6 @@ func loadConfig(filename string) error { commands.GlobalArgs.Talosconfig = commands.Config.GlobalOptions.Talosconfig } - if commands.Config.TemplateOptions.KubernetesVersion == "" { - commands.Config.TemplateOptions.KubernetesVersion = constants.DefaultKubernetesVersion - } - // Fill in the default-string path BEFORE parsing so both the // "operator left timeout empty" and "operator supplied a value" // branches end up with TimeoutDuration populated. The previous diff --git a/main_test.go b/main_test.go index 2a93cbf6..d22e0d0d 100644 --- a/main_test.go +++ b/main_test.go @@ -748,3 +748,32 @@ func TestReleaseVersion(t *testing.T) { }) } } + +// TestLoadConfig_EmptyKubernetesVersionStaysEmpty pins that an unset +// templateOptions.kubernetesVersion reaches the engine as an empty string. +// +// Substituting the machinery default here would move a cluster's Kubernetes +// version on a talm upgrade — and invisibly, since the images equal the bundle +// default and the render's diff drops them. The engine is what decides what an +// unset version means: on contracts where the node can still choose it emits no +// image at all, and on the ones where the typed documents require an image it +// reports the missing pin. Neither is reachable if the value arrives filled in. +func TestLoadConfig_EmptyKubernetesVersionStaysEmpty(t *testing.T) { + dir := t.TempDir() + chartPath := filepath.Join(dir, "Chart.yaml") + body := "apiVersion: v2\nname: test\nversion: 0.1.0\ntemplateOptions:\n kubernetesVersion: \"\"\n" + + if err := os.WriteFile(chartPath, []byte(body), 0o644); err != nil { + t.Fatalf("write Chart.yaml: %v", err) + } + + snapshotConfigState(t) + + if err := loadConfig(chartPath); err != nil { + t.Fatalf("loadConfig: %v", err) + } + + if got := commands.Config.TemplateOptions.KubernetesVersion; got != "" { + t.Errorf("an unset kubernetesVersion reached the engine as %q; it must stay empty so the engine decides", got) + } +} diff --git a/pkg/applycheck/refs_netaddr_test.go b/pkg/applycheck/refs_netaddr_test.go index 0df8148f..a1cfa09f 100644 --- a/pkg/applycheck/refs_netaddr_test.go +++ b/pkg/applycheck/refs_netaddr_test.go @@ -362,29 +362,43 @@ func TestWalkNetAddrFindings_RealSchema_NetworkRuleConfig(t *testing.T) { } } -// TestMultidocNetAddrHandlers_OverlapEmitsNoValidatedRefs pins what the two -// dispatch maps must not do together: report the same thing twice. +// TestMultidocNetAddrHandlers_OverlapIsDeclared pins what the two dispatch maps +// must not do together: report the same thing twice. // // A kind may appear in both — WireguardConfig does, because the net-addr walker // checks its peer endpoints while the ref walker records the link it creates. -// That is safe only while the ref side emits nothing that gets validated: a -// created-link ref seeds the known-links set and produces no finding of its own. -// A kind that emitted an existing-link ref from one map and a finding from the -// other would surface the same mistake twice. -func TestMultidocNetAddrHandlers_OverlapEmitsNoValidatedRefs(t *testing.T) { +// That is safe only while the ref side emits nothing that gets validated, which +// is a property of the handler, not of any one document. So the overlap is +// declared here by name rather than probed: a probe only exercises the fields it +// happens to carry, and a future handler reading a different key would slip past +// it while duplicating findings for real. +func TestMultidocNetAddrHandlers_OverlapIsDeclared(t *testing.T) { t.Parallel() + // Kinds allowed in both maps, each because its ref-side handler only records + // a created link. + declared := map[string]struct{}{ + wireguardConfigKind: {}, + } + for kind := range multidocNetAddrHandlers { - handler, overlaps := multidocHandlers[kind] - if !overlaps { + if _, overlaps := multidocHandlers[kind]; !overlaps { continue } - refs := handler(nil, map[string]any{"name": "probe0"}, "doc[0]") - for _, ref := range refs { - if ref.Kind != RefKindLinkCreated { - t.Errorf("kind %q is in both dispatch maps and emits a validated ref (%v) — the same mistake would be reported twice", kind, ref.Kind) - } + if _, allowed := declared[kind]; !allowed { + t.Errorf("kind %q is in both dispatch maps without being declared here; confirm its ref handler emits only created-link refs, then add it", kind) + } + } + + // The declaration has to stay honest: every kind listed must still be in both + // maps, or it is stale cover for an overlap that no longer exists. + for kind := range declared { + _, inRefs := multidocHandlers[kind] + _, inNetAddr := multidocNetAddrHandlers[kind] + + if !inRefs || !inNetAddr { + t.Errorf("kind %q is declared as an allowed overlap but is no longer in both maps", kind) } } } diff --git a/pkg/applycheck/validate_test.go b/pkg/applycheck/validate_test.go index 8fd2f4d8..45066d2d 100644 --- a/pkg/applycheck/validate_test.go +++ b/pkg/applycheck/validate_test.go @@ -648,10 +648,10 @@ parent: bond9 } } -// A wireguard link is created by the apply, exactly like a bond or a dummy: -// a wireguard link is one the apply brings into existence, not one the node is -// expected to already carry. A VLAN parented to it, or a VIP on it, is a -// config Talos accepts, so neither may block. +// A wireguard link is one this apply brings into existence, exactly like a bond +// or a dummy, not a reference to something the node is expected to carry +// already. Blocking a document that points at it would reject the apply that +// creates it. // // WireguardConfig is dispatched by both walkers — the net-addr one validates its // peer endpoints — which is safe because the ref side only records the created diff --git a/pkg/commands/apply.go b/pkg/commands/apply.go index b91eef47..c52ef875 100644 --- a/pkg/commands/apply.go +++ b/pkg/commands/apply.go @@ -567,12 +567,12 @@ func applyOneFileDirectPatchMode(configFile, withSecretsPath string) error { ) } - result, err := engine.SerializeConfiguration(configBundle, machineType) + result, err := engine.SerializeConfiguration(configBundle, machineType, opts.TalosVersion, opts.KubernetesVersion) if err != nil { //nolint:wrapcheck // already wrapped via errors.Wrap, WithHint adds operator-facing guidance return errors.WithHint( errors.Wrap(err, "serializing configuration"), - "the merged config bundle could not be encoded back to YAML; this is internal — file an issue if reproducible", + "if the message above names a templateOptions key or a v1alpha1 conflict, it is the project's Chart.yaml to fix; anything else is internal — file an issue if reproducible", ) } @@ -1240,11 +1240,11 @@ func init() { applyCmd.Flags().StringArrayVar(&applyCmdFlags.literalValues, "set-literal", []string{}, "set a literal STRING value on the command line") applyCmd.Flags().StringVar(&applyCmdFlags.talosVersion, "talos-version", "", "the desired Talos version to generate config for (backwards compatibility, e.g. v0.8)") applyCmd.Flags().StringVar(&applyCmdFlags.withSecrets, "with-secrets", "", "use a secrets file generated using 'gen secrets'") - applyCmd.Flags().StringVar(&applyCmdFlags.kubernetesVersion, "kubernetes-version", constants.DefaultKubernetesVersion, "desired kubernetes version to run") + applyCmd.Flags().StringVar(&applyCmdFlags.kubernetesVersion, "kubernetes-version", "", "desired kubernetes version to run; defaults to templateOptions.kubernetesVersion from Chart.yaml") applyCmd.Flags().BoolVarP(&applyCmdFlags.debug, "debug", "", false, "show only rendered patches") applyCmd.Flags().BoolVar(&applyCmdFlags.dryRun, "dry-run", false, "check how the config change will be applied in dry-run mode") applyCmd.Flags().DurationVar(&applyCmdFlags.configTryTimeout, "timeout", constants.ConfigTryTimeout, "the config will be rolled back after specified timeout (if try mode is selected)") - applyCmd.Flags().StringSliceVar(&applyCmdFlags.certFingerprints, "cert-fingerprint", nil, "list of server certificate fingeprints to accept (defaults to no check)") + applyCmd.Flags().StringSliceVar(&applyCmdFlags.certFingerprints, "cert-fingerprint", nil, "list of server certificate fingerprints to accept (defaults to no check)") applyCmd.Flags().BoolVar(&applyCmdFlags.force, "force", false, "will overwrite existing files") applyCmd.Flags().BoolVar(&applyCmdFlags.skipResourceValidation, "skip-resource-validation", false, "skip the pre-apply check that declared host resources (links, disks) exist on the target node") applyCmd.Flags().BoolVar(&applyCmdFlags.skipDriftPreview, "skip-drift-preview", false, "skip the pre-apply diff of on-node vs rendered MachineConfig") diff --git a/pkg/commands/contract_stdout_silence_test.go b/pkg/commands/contract_stdout_silence_test.go index eb2396ae..ff2c37cb 100644 --- a/pkg/commands/contract_stdout_silence_test.go +++ b/pkg/commands/contract_stdout_silence_test.go @@ -188,6 +188,7 @@ func TestContract_TemplateProgress_GoesToStderr(t *testing.T) { Config.RootDir = root templateCmdFlags.configFiles = []string{nodeFile} templateCmdFlags.offline = true + templateCmdFlags.talosVersion = "v1.13" templateCmdFlags.templatesFromArgs = false templateCmdFlags.nodesFromArgs = false templateCmdFlags.endpointsFromArgs = false diff --git a/pkg/commands/contract_template_test.go b/pkg/commands/contract_template_test.go index a2002210..75dc49cc 100644 --- a/pkg/commands/contract_template_test.go +++ b/pkg/commands/contract_template_test.go @@ -31,6 +31,7 @@ import ( "testing" "github.com/siderolabs/talos/pkg/machinery/config/generate/secrets" + "github.com/spf13/cobra" "gopkg.in/yaml.v3" ) @@ -200,6 +201,9 @@ func TestContract_GenerateOutput_ComposesModelineWarningAndRender(t *testing.T) }{ offline: true, templateFiles: []string{testTemplateConfig}, + // A contract from before the Kubernetes settings moved into their own + // documents, where an unpinned Kubernetes version is still valid. + talosVersion: "v1.13", } GlobalArgs.Nodes = []string{testNodeAddrA} GlobalArgs.Endpoints = []string{testNodeAddrA} @@ -242,6 +246,7 @@ func TestContract_GenerateOutput_MissingTemplateError(t *testing.T) { chartRoot := makeMinimalChart(t) Config.RootDir = chartRoot templateCmdFlags.offline = true + templateCmdFlags.talosVersion = "v1.13" templateCmdFlags.templateFiles = []string{"templates/does-not-exist.yaml"} GlobalArgs.Nodes = []string{testNodeAddrA} @@ -262,6 +267,7 @@ func TestContract_GenerateOutput_NoTemplatesError(t *testing.T) { chartRoot := makeMinimalChart(t) Config.RootDir = chartRoot templateCmdFlags.offline = true + templateCmdFlags.talosVersion = "v1.13" templateCmdFlags.templateFiles = nil GlobalArgs.Nodes = []string{testNodeAddrA} @@ -282,6 +288,7 @@ func TestContract_Template_PrintsToStdout(t *testing.T) { chartRoot := makeMinimalChart(t) Config.RootDir = chartRoot templateCmdFlags.offline = true + templateCmdFlags.talosVersion = "v1.13" templateCmdFlags.templateFiles = []string{testTemplateConfig} GlobalArgs.Nodes = []string{testNodeAddrA} GlobalArgs.Endpoints = []string{testNodeAddrA} @@ -310,6 +317,7 @@ func TestContract_Template_PropagatesError(t *testing.T) { chartRoot := makeMinimalChart(t) Config.RootDir = chartRoot templateCmdFlags.offline = true + templateCmdFlags.talosVersion = "v1.13" templateCmdFlags.templateFiles = []string{testTemplateMissing} GlobalArgs.Nodes = []string{testNodeAddrA} @@ -415,3 +423,23 @@ func captureStdout(t *testing.T, fn func()) string { _ = w.Close() return <-done } + +// TestContract_KubernetesVersionFlagDefaultsEmpty pins that the flag advertises +// no version of its own. +// +// PreRunE overwrites the flag with the Chart.yaml value whenever the operator +// did not pass it, so a registered default is unreachable — and a reachable one +// would mean an unpinned project silently adopts the Kubernetes version talm +// was built with. `--talos-version` already registers empty for the same reason. +func TestContract_KubernetesVersionFlagDefaultsEmpty(t *testing.T) { + for _, cmd := range []*cobra.Command{templateCmd, applyCmd} { + flag := cmd.Flags().Lookup("kubernetes-version") + if flag == nil { + t.Fatalf("%s has no --kubernetes-version flag", cmd.Name()) + } + + if flag.DefValue != "" { + t.Errorf("%s advertises --kubernetes-version default %q; an unpinned project must not get a version it never chose", cmd.Name(), flag.DefValue) + } + } +} diff --git a/pkg/commands/preflight_upgrade_verify_test.go b/pkg/commands/preflight_upgrade_verify_test.go index 08bb51d8..5a76b374 100644 --- a/pkg/commands/preflight_upgrade_verify_test.go +++ b/pkg/commands/preflight_upgrade_verify_test.go @@ -24,11 +24,10 @@ import ( "github.com/cockroachdb/errors" ) -// TestShouldRunPostUpgradeVerify_SkipMatrix pins the predicate that -// gates Phase 2C scheduling. The gate cannot produce a meaningful -// result on --insecure (no auth COSI path) or --stage (new partition -// not yet booted), which must be skipped to avoid a false-positive -// blocker. The skip flag overrides everything (operator opt-out). +// TestShouldRunPostUpgradeVerify_SkipMatrix pins Phase 2C scheduling. The verify +// cannot produce a meaningful result after --stage (the new partition is not yet +// booted, so runtime.Version still reports the old version), and the skip flag +// overrides everything as an operator opt-out. // // Talos v1.14 removed upgrade's --insecure, which used to be the other skip. func TestShouldRunPostUpgradeVerify_SkipMatrix(t *testing.T) { diff --git a/pkg/commands/template.go b/pkg/commands/template.go index 7fb55b98..79bf96a8 100644 --- a/pkg/commands/template.go +++ b/pkg/commands/template.go @@ -28,7 +28,6 @@ import ( "github.com/spf13/cobra" "github.com/siderolabs/talos/pkg/machinery/client" - "github.com/siderolabs/talos/pkg/machinery/constants" ) //nolint:gochecknoglobals // cobra command flag struct, idiomatic for cobra-based CLIs @@ -502,7 +501,7 @@ func init() { templateCmd.Flags().BoolVarP(&templateCmdFlags.debug, "debug", "", false, "show only rendered patches") templateCmd.Flags().BoolVarP(&templateCmdFlags.offline, "offline", "", false, "disable gathering information and lookup functions") templateCmd.Flags().BoolVar(&templateCmdFlags.showSecrets, "show-secrets", false, "print values from encrypted value files (*.encrypted.yaml) verbatim in stdout output (default: redacted to ***; never affects -I, which always omits them). Counterpart on apply is --show-secrets-in-drift, which governs the same values in apply's drift preview.") - templateCmd.Flags().StringVar(&templateCmdFlags.kubernetesVersion, "kubernetes-version", constants.DefaultKubernetesVersion, "desired kubernetes version to run") + templateCmd.Flags().StringVar(&templateCmdFlags.kubernetesVersion, "kubernetes-version", "", "desired kubernetes version to run; defaults to templateOptions.kubernetesVersion from Chart.yaml") // Shell completion for `talm template` flags. `--file` uses the // modelined-yaml lister (same as apply); other yaml-shaped flags diff --git a/pkg/engine/contract_multidoc_render_test.go b/pkg/engine/contract_multidoc_render_test.go index 4243be2f..51e7daad 100644 --- a/pkg/engine/contract_multidoc_render_test.go +++ b/pkg/engine/contract_multidoc_render_test.go @@ -112,13 +112,12 @@ func TestContract_RenderLeavesV1Alpha1ComponentsAloneOnMultidocContract(t *testi // it needs to join, which the node file itself does not carry. func TestContract_ApplyPathCarriesEveryDocument(t *testing.T) { opts := Options{KubernetesVersion: "v1.34.3", TalosVersion: "v1.14"} + // No cluster.controlPlane.endpoint: on this contract that field lives in + // KubeClusterConfig, and setting both is the conflict the render refuses. patch := `machine: type: controlplane install: disk: /dev/sda -cluster: - controlPlane: - endpoint: https://192.0.2.4:6443 ` configBundle, machineType, err := FullConfigProcess(opts, []string{patch}) @@ -126,7 +125,7 @@ cluster: t.Fatalf("FullConfigProcess: %v", err) } - out, err := SerializeConfiguration(configBundle, machineType) + out, err := SerializeConfiguration(configBundle, machineType, opts.TalosVersion, opts.KubernetesVersion) if err != nil { t.Fatalf("SerializeConfiguration: %v", err) } diff --git a/pkg/engine/contract_render_test.go b/pkg/engine/contract_render_test.go index ec4b7ecd..57f693fe 100644 --- a/pkg/engine/contract_render_test.go +++ b/pkg/engine/contract_render_test.go @@ -90,6 +90,7 @@ func TestContract_Render_NoTemplateFilesShortCircuitsBeforeRender(t *testing.T) func TestContract_Render_TemplateNotFoundError(t *testing.T) { chartRoot := createTestChart(t, "tc", "config.yaml", "machine:\n type: worker\n") _, err := Render(context.Background(), nil, Options{ + TalosVersion: legacyContract, Offline: true, Root: chartRoot, TemplateFiles: []string{"templates/does-not-exist.yaml"}, @@ -107,6 +108,7 @@ func TestContract_Render_TemplateNotFoundError(t *testing.T) { func TestContract_Render_ChartLoadError(t *testing.T) { bogus := filepath.Join(t.TempDir(), "no-such-chart") _, err := Render(context.Background(), nil, Options{ + TalosVersion: legacyContract, Offline: true, Root: bogus, TemplateFiles: []string{"templates/config.yaml"}, @@ -122,6 +124,7 @@ func TestContract_Render_ChartLoadError(t *testing.T) { func TestContract_Render_BadValueFileError(t *testing.T) { chartRoot := createTestChart(t, "tc", "config.yaml", "machine:\n type: worker\n") _, err := Render(context.Background(), nil, Options{ + TalosVersion: legacyContract, Offline: true, Root: chartRoot, ValueFiles: []string{"/path/that/does/not/exist.yaml"}, @@ -144,6 +147,7 @@ func TestContract_Render_BadValueFileError(t *testing.T) { func TestContract_Render_HappyPathOfflineWorker(t *testing.T) { chartRoot := createTestChart(t, "tc", "config.yaml", "machine:\n type: worker\n") out, err := Render(context.Background(), nil, Options{ + TalosVersion: legacyContract, Offline: true, Root: chartRoot, TemplateFiles: []string{"templates/config.yaml"}, @@ -171,6 +175,7 @@ func TestContract_Render_SetValuesReachTemplate(t *testing.T) { ` chartRoot := createTestChart(t, "tc", "config.yaml", tmpl) out, err := Render(context.Background(), nil, Options{ + TalosVersion: legacyContract, Offline: true, Root: chartRoot, Values: []string{"customImage=registry.example.com/talos:test"}, @@ -241,7 +246,8 @@ func TestContract_InitializeConfigBundle_BadTalosVersionError(t *testing.T) { // surfaces a 'failed to load secrets bundle' error naming the cause. func TestContract_InitializeConfigBundle_MissingSecretsError(t *testing.T) { _, err := InitializeConfigBundle(Options{ - WithSecrets: filepath.Join(t.TempDir(), "missing-secrets.yaml"), + TalosVersion: legacyContract, + WithSecrets: filepath.Join(t.TempDir(), "missing-secrets.yaml"), }) if err == nil { t.Fatal("expected error") @@ -256,18 +262,26 @@ func TestContract_InitializeConfigBundle_MissingSecretsError(t *testing.T) { // controlplane serialization is meaningfully different from the // worker one (controlplane has cluster.* fields worker does not). func TestContract_SerializeConfiguration_ControlplaneVsWorker(t *testing.T) { - b, err := InitializeConfigBundle(Options{}) + b, err := InitializeConfigBundle(Options{TalosVersion: legacyContract, KubernetesVersion: "v1.34.3"}) if err != nil { t.Fatal(err) } - cpBytes, err := SerializeConfiguration(b, machine.TypeControlPlane) + cpBytes, err := SerializeConfiguration(b, machine.TypeControlPlane, legacyContract, "v1.34.3") if err != nil { t.Fatalf("controlplane: %v", err) } - workerBytes, err := SerializeConfiguration(b, machine.TypeWorker) + workerBytes, err := SerializeConfiguration(b, machine.TypeWorker, legacyContract, "v1.34.3") if err != nil { t.Fatalf("worker: %v", err) } + // The contract passed to SerializeConfiguration has to be the one the bundle + // was built on: mismatch them and the images are stripped out of documents + // that require one, producing a config the node rejects while the test still + // sees non-empty bytes. + if !strings.Contains(string(cpBytes), "image:") { + t.Errorf("controlplane serialization carries no component image:\n%s", cpBytes) + } + if len(cpBytes) == 0 || len(workerBytes) == 0 { t.Fatal("expected non-empty serialization") } @@ -356,6 +370,7 @@ func TestContract_Render_OfflineSkipsMultiNodeCheck(t *testing.T) { // Context with multiple nodes — would trip FailIfMultiNodes online. ctx := context.Background() _, err := Render(ctx, nil, Options{ + TalosVersion: legacyContract, Offline: true, Root: chartRoot, TemplateFiles: []string{"templates/config.yaml"}, diff --git a/pkg/engine/contract_validate_render_test.go b/pkg/engine/contract_validate_render_test.go new file mode 100644 index 00000000..92c3ef10 --- /dev/null +++ b/pkg/engine/contract_validate_render_test.go @@ -0,0 +1,506 @@ +package engine + +import ( + "bytes" + "io" + "os" + "strings" + "testing" + + "github.com/cockroachdb/errors" + "github.com/siderolabs/talos/pkg/machinery/config/configloader" + "github.com/siderolabs/talos/pkg/machinery/constants" +) + +// legacyContract is a Talos contract from before the Kubernetes settings moved +// into documents of their own, where an unpinned Kubernetes version is still +// something the node can decide. +const legacyContract = "v1.13" + +// metalMode is the runtime mode a rendered config is validated against: a real +// machine that installs to disk. +type metalMode struct{} + +func (metalMode) String() string { return "metal" } +func (metalMode) RequiresInstall() bool { return true } +func (metalMode) InContainer() bool { return false } + +// validateRendered loads a rendered config the way a node does and returns the +// validation error, if any. +func validateRendered(t *testing.T, rendered []byte) error { + t.Helper() + + cfg, err := configloader.NewFromBytes(rendered) + if err != nil { + t.Fatalf("loading rendered config: %v", err) + } + + _, err = cfg.ValidateAsClient(metalMode{}) + + //nolint:wrapcheck // the caller inspects this error verbatim. + return err +} + +// A render must not hand the operator a config that mixes a v1alpha1 field with +// the document that replaced it: machinery rejects it, so the node refuses the +// apply. The charts write v1alpha1 fields, so a project pinned at or below the +// contract where those fields still live renders cleanly. +// +// Nothing else in the suite loads a rendered config through machinery, which is +// how a major Talos bump could rewrite the output and stay green. +func TestContract_RenderedConfigValidates(t *testing.T) { + for _, talosVersion := range []string{"v1.12", "v1.13"} { + t.Run("talosVersion="+talosVersion, func(t *testing.T) { + out, err := applyPatchesAndRenderConfig( + Options{KubernetesVersion: "v1.34.3", TalosVersion: talosVersion, Full: true}, + []string{chartShapedPatch}) + if err != nil { + t.Fatalf("applyPatchesAndRenderConfig: %v", err) + } + + if err := validateRendered(t, out); err != nil { + t.Errorf("rendered config does not validate: %v", err) + } + }) + } +} + +// An unpinned project renders against the Talos version talm was built from. The +// shipped charts still write the v1alpha1 fields that version superseded, so the +// render has to stop with a way out rather than emit a config the node rejects +// at apply time. +func TestContract_UnpinnedRenderReportsSupersededFields(t *testing.T) { + _, err := applyPatchesAndRenderConfig( + Options{KubernetesVersion: "v1.34.3", Full: true}, + []string{chartShapedPatch}) + if err == nil { + t.Fatal("expected the render to report the superseded v1alpha1 fields") + } + + if !strings.Contains(err.Error(), "mixes v1alpha1 fields") { + t.Errorf("error does not name the conflict: %v", err) + } + + // The same has to hold when the chart emits a typed document of its own: the + // bundle emits one too, and a naive concatenation of the two is rejected as a + // duplicate before the conflict check ever runs. + _, err = applyPatchesAndRenderConfig( + Options{KubernetesVersion: "v1.34.3", Full: true}, + []string{chartShapedPatch + "---\napiVersion: v1alpha1\nkind: HostnameConfig\nhostname: node0\n"}) + if err == nil { + t.Error("a chart document of the same kind as the bundle's silenced the conflict check") + } else if !strings.Contains(err.Error(), "mixes v1alpha1 fields") { + t.Errorf("error does not name the conflict: %v", err) + } + + hint := errors.FlattenHints(err) + if !strings.Contains(hint, "talosVersion") { + t.Errorf("error carries no hint pointing at talosVersion, got hints: %q", hint) + } + + // Validation reports unrelated incompleteness at the same time; surfacing it + // here would bury the real cause under false leads the operator is expected + // to fill in anyway. + for _, unrelated := range []string{"clusterName must be specified", "endpoint must be specified", "service issuer URL is required"} { + if strings.Contains(err.Error(), unrelated) { + t.Errorf("error carries the unrelated %q alongside the conflict:\n%v", unrelated, err) + } + } +} + +// chartShapedPatch carries the v1alpha1 fields the shipped charts write and +// Talos later moved into documents of their own. +const chartShapedPatch = `machine: + type: controlplane + kubelet: + extraArgs: + rotate-server-certificates: "true" + network: + nameservers: + - 192.0.2.53 + install: + disk: /dev/sda +cluster: + controlPlane: + endpoint: https://192.0.2.4:6443 +` + +func TestKubeVersion(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + in string + want string + }{ + {"unset falls back to the machinery default", "", strings.TrimPrefix(constants.DefaultKubernetesVersion, "v")}, + {"v prefix is stripped", "v1.30.0", "1.30.0"}, + {"bare version is passed through", "1.30.0", "1.30.0"}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + if got := kubeVersion(tc.in); got != tc.want { + t.Errorf("kubeVersion(%q) = %q, want %q", tc.in, got, tc.want) + } + }) + } +} + +// On a contract where the node can still choose, a project that pins no +// kubernetesVersion must not have one chosen for it: the fallback that satisfies +// v1.14's generator would otherwise write this binary's Kubernetes version into +// the config, invisibly, since the render's diff drops fields equal to the +// bundle default. +func TestContract_UnsetKubernetesVersionEmitsNoImages(t *testing.T) { + patch := `machine: + type: controlplane + install: + disk: /dev/sda +cluster: + controlPlane: + endpoint: https://192.0.2.4:6443 +` + + assertNoKubeImages := func(t *testing.T, what string, out []byte) { + t.Helper() + + for _, line := range strings.Split(string(out), "\n") { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "image:") && (strings.Contains(trimmed, "kube") || strings.Contains(trimmed, "kubelet")) { + t.Errorf("%s pinned a Kubernetes version the project never set: %s", what, trimmed) + } + } + } + + for _, talosVersion := range []string{"v1.12", legacyContract} { + t.Run("talosVersion="+talosVersion, func(t *testing.T) { + rendered, err := applyPatchesAndRenderConfig( + Options{TalosVersion: talosVersion, Full: true}, + []string{patch}) + if err != nil { + t.Fatalf("applyPatchesAndRenderConfig: %v", err) + } + + assertNoKubeImages(t, "the render", rendered) + + // The direct-patch apply path serializes the bundle itself and never + // goes through the render, so it needs its own assertion. + configBundle, machineType, err := FullConfigProcess(Options{TalosVersion: talosVersion}, []string{patch}) + if err != nil { + t.Fatalf("FullConfigProcess: %v", err) + } + + sent, err := SerializeConfiguration(configBundle, machineType, talosVersion, "") + if err != nil { + t.Fatalf("SerializeConfiguration: %v", err) + } + + assertNoKubeImages(t, "the apply path", sent) + }) + } +} + +// From the contract where the Kubernetes settings live in documents of their +// own, those documents require an image and there is no node-side default left. +// Leaving the version unset has to be reported, not papered over with a config +// machinery rejects for an empty image. +func TestContract_UnsetKubernetesVersionRefusedOnMultidocContract(t *testing.T) { + patch := `machine: + type: controlplane + install: + disk: /dev/sda +cluster: + controlPlane: + endpoint: https://192.0.2.4:6443 +` + + for _, talosVersion := range []string{"", "v1.14"} { + t.Run("talosVersion="+talosVersion, func(t *testing.T) { + _, err := applyPatchesAndRenderConfig( + Options{TalosVersion: talosVersion, Full: true}, + []string{patch}) + if err == nil { + t.Fatal("an unset Kubernetes version must be reported on this contract") + } + + if !strings.Contains(errors.FlattenHints(err), "kubernetesVersion") { + t.Errorf("error carries no hint naming the key to pin: %v", err) + } + }) + } +} + +// An image the operator wrote themselves is theirs, pinned version or not. +// Stripping it would silently redirect an airgapped cluster away from its +// mirror, and the drift preview would compare against the stripped config. +func TestContract_UnsetKubernetesVersionKeepsOperatorImages(t *testing.T) { + patch := `machine: + type: controlplane + install: + disk: /dev/sda + kubelet: + image: ghcr.io/example/kubelet:v1.31.0-custom +cluster: + controlPlane: + endpoint: https://192.0.2.4:6443 + apiServer: + image: registry.example.com/mirror/kube-apiserver:v1.31.0 +` + + out, err := applyPatchesAndRenderConfig( + Options{TalosVersion: "v1.13", Full: true}, + []string{patch}) + if err != nil { + t.Fatalf("applyPatchesAndRenderConfig: %v", err) + } + + for _, want := range []string{ + "ghcr.io/example/kubelet:v1.31.0-custom", + "registry.example.com/mirror/kube-apiserver:v1.31.0", + } { + if !strings.Contains(string(out), want) { + t.Errorf("the operator's own image %q was stripped\n--- rendered ---\n%s", want, out) + } + } +} + +// Pinning the version is what puts the images in, and every component lands on +// the version that was pinned. +func TestContract_PinnedKubernetesVersionReachesEveryComponent(t *testing.T) { + patch := `machine: + type: controlplane + install: + disk: /dev/sda +cluster: + controlPlane: + endpoint: https://192.0.2.4:6443 +` + + out, err := applyPatchesAndRenderConfig( + Options{KubernetesVersion: "v1.34.3", TalosVersion: "v1.13", Full: true}, + []string{patch}) + if err != nil { + t.Fatalf("applyPatchesAndRenderConfig: %v", err) + } + + for _, want := range []string{ + "ghcr.io/siderolabs/kubelet:v1.34.3", + "registry.k8s.io/kube-apiserver:v1.34.3", + "registry.k8s.io/kube-controller-manager:v1.34.3", + "registry.k8s.io/kube-proxy:v1.34.3", + "registry.k8s.io/kube-scheduler:v1.34.3", + } { + if !strings.Contains(string(out), want) { + t.Errorf("rendered config missing %q\n--- rendered ---\n%s", want, out) + } + } +} + +// Machinery phrases the conflict two ways, and the k8s documents use the form +// with an article. Matching only the other one leaves the check disabled for +// .cluster.controlPlane.endpoint and .cluster.clusterName, which every chart +// writes — the exact fields an operator is most likely to hit. +func TestContract_ClusterEndpointConflictIsReported(t *testing.T) { + patch := `machine: + type: controlplane + install: + disk: /dev/sda +cluster: + controlPlane: + endpoint: https://192.0.2.4:6443 +` + + _, err := applyPatchesAndRenderConfig( + Options{KubernetesVersion: "v1.34.3", TalosVersion: "v1.14", Full: true}, + []string{patch}) + if err == nil { + t.Fatal("a superseded cluster endpoint must stop the render") + } + + if !strings.Contains(err.Error(), "mixes v1alpha1 fields") { + t.Errorf("error is not recognised as the conflict class: %v", err) + } +} + +// The check asks each document whether it conflicts, rather than reading +// validation text, so a kind whose message is phrased differently is still +// caught. KubeSpanConfig and KubeProxyConfig both word it their own way. +func TestContract_SupersededFieldsCaughtRegardlessOfWording(t *testing.T) { + for _, tc := range []struct { + name string + patch string + }{ + { + name: "kubespan", + patch: `machine: + type: controlplane + install: + disk: /dev/sda + network: + kubespan: + enabled: true +--- +apiVersion: v1alpha1 +kind: KubeSpanConfig +enabled: true +`, + }, + { + name: "proxy", + patch: `machine: + type: controlplane + install: + disk: /dev/sda +cluster: + proxy: + disabled: true +--- +apiVersion: v1alpha1 +kind: KubeProxyConfig +enabled: false +image: registry.k8s.io/kube-proxy:v1.34.3 +`, + }, + } { + t.Run(tc.name, func(t *testing.T) { + _, err := applyPatchesAndRenderConfig( + Options{KubernetesVersion: "v1.34.3", TalosVersion: "v1.14", Full: true}, + []string{tc.patch}) + if err == nil { + t.Fatal("a document superseding a v1alpha1 field must stop the render") + } + + if !strings.Contains(err.Error(), "mixes v1alpha1 fields") { + t.Errorf("error is not the conflict class: %v", err) + } + }) + } +} + +// The warning belongs to the render, which serializes the bundle twice on the +// default path. Emitting it from the serializer said it twice. +func TestContract_UnpinnedKubernetesVersionWarnsOnce(t *testing.T) { + contract, err := renderContract(legacyContract) + if err != nil { + t.Fatalf("renderContract: %v", err) + } + + var buf bytes.Buffer + + warnUnpinnedKubernetesVersion(&buf, contract, "") + + if got := strings.Count(buf.String(), "warning:"); got != 1 { + t.Errorf("warning emitted %d times, want 1:\n%s", got, buf.String()) + } + + buf.Reset() + warnUnpinnedKubernetesVersion(&buf, contract, "v1.34.3") + + if buf.Len() != 0 { + t.Errorf("a pinned version must not warn, got:\n%s", buf.String()) + } +} + +// The direct-patch apply path never goes through the render, so it carries its +// own copy of both guards. Without them a node file with no templates would be +// serialized with the component images stripped and sent on, silently. +func TestContract_DirectPatchPathWarnsAndChecks(t *testing.T) { + patch := `machine: + type: controlplane + install: + disk: /dev/sda +` + + configBundle, machineType, err := FullConfigProcess(Options{TalosVersion: legacyContract}, []string{patch}) + if err != nil { + t.Fatalf("FullConfigProcess: %v", err) + } + + stderr := captureStderr(t, func() { + if _, err := SerializeConfiguration(configBundle, machineType, legacyContract, ""); err != nil { + t.Fatalf("SerializeConfiguration: %v", err) + } + }) + + if !strings.Contains(stderr, "kubernetesVersion is not set") { + t.Errorf("the direct-patch path stripped the component images without saying so, stderr:\n%s", stderr) + } +} + +// captureStderr runs fn with os.Stderr redirected and returns what it wrote. +func captureStderr(t *testing.T, fn func()) string { + t.Helper() + + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("pipe: %v", err) + } + + saved := os.Stderr + os.Stderr = w + + // fn may call t.Fatalf, which ends the goroutine before the restore below. + // Without this the next test writes into a pipe nobody reads. + t.Cleanup(func() { + os.Stderr = saved + _ = w.Close() + _ = r.Close() + }) + + fn() + + os.Stderr = saved + + if err := w.Close(); err != nil { + t.Fatalf("close pipe: %v", err) + } + + out, err := io.ReadAll(r) + if err != nil { + t.Fatalf("read pipe: %v", err) + } + + return string(out) +} + +// The conflict check fires on any document that supersedes a v1alpha1 field, +// including on a contract that still keeps those settings in v1alpha1 — a chart +// emitting both shapes at once. Telling that operator to pin the contract lower +// sends them nowhere: they are already below the boundary, and the fix is to +// drop one of the two shapes. +func TestContract_SupersededFieldsHintMatchesTheContract(t *testing.T) { + rendered := `machine: + type: controlplane + network: + hostname: node0 +--- +apiVersion: v1alpha1 +kind: HostnameConfig +hostname: node0 +` + + for _, tc := range []struct { + name string + talosVersion string + wantPinLower bool + }{ + {name: "above the boundary", talosVersion: "v1.14", wantPinLower: true}, + {name: "at the boundary", talosVersion: "v1.13", wantPinLower: false}, + {name: "below the boundary", talosVersion: "v1.12", wantPinLower: false}, + } { + t.Run(tc.name, func(t *testing.T) { + err := checkSupersededFields([]byte(rendered), tc.talosVersion) + if err == nil { + t.Fatalf("talosVersion=%s: expected a conflict", tc.talosVersion) + } + + hints := strings.Join(errors.GetAllHints(err), "\n") + + if got := strings.Contains(hints, "Pin it to"); got != tc.wantPinLower { + t.Errorf("talosVersion=%s: hint offers a lower pin = %v, want %v:\n%s", + tc.talosVersion, got, tc.wantPinLower, hints) + } + }) + } +} diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index a8956a94..62f621ea 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -32,7 +32,9 @@ import ( "github.com/siderolabs/talos/pkg/machinery/client" "github.com/siderolabs/talos/pkg/machinery/config" "github.com/siderolabs/talos/pkg/machinery/config/bundle" + "github.com/siderolabs/talos/pkg/machinery/config/configloader" "github.com/siderolabs/talos/pkg/machinery/config/configpatcher" + "github.com/siderolabs/talos/pkg/machinery/config/container" "github.com/siderolabs/talos/pkg/machinery/config/encoder" "github.com/siderolabs/talos/pkg/machinery/config/generate" "github.com/siderolabs/talos/pkg/machinery/config/generate/secrets" @@ -192,16 +194,31 @@ func FullConfigProcess(opts Options, patches []string) (*bundle.Bundle, machine. return configBundle, machineType, nil } +// renderContract resolves the contract a render targets. An unset talosVersion +// means the version of Talos this binary was built against, which is what +// machinery defaults to on its own. +func renderContract(talosVersion string) (*config.VersionContract, error) { + if talosVersion == "" { + return config.TalosVersionCurrent, nil + } + + contract, err := config.ParseContractFromVersion(talosVersion) + if err != nil { + return nil, errors.Wrap(err, "invalid talos-version") + } + + return contract, nil +} + // kubeVersion returns the Kubernetes version (without the leading "v") for the // config bundle, falling back to the version this binary's machinery was built // against when unset. // // Talos v1.14's config/generate errors on an empty version, where earlier -// machinery emitted no image fields at all and left every component to the -// node's own default. The fallback is therefore a behaviour change for a project -// that pins nothing: it gets this binary's Kubernetes version instead of the -// node's. Both shipped presets pin the key, and docs/configuration/talos-versions.md -// tells operators to do the same. +// machinery emitted no image fields at all and left every component to the node's +// own default. The fallback exists to satisfy the generator; stripDefaultedImages +// then removes what it produced, so an unpinned project keeps the earlier +// behaviour rather than silently adopting this binary's Kubernetes version. func kubeVersion(v string) string { if v == "" { v = constants.DefaultKubernetesVersion @@ -216,15 +233,15 @@ func kubeVersion(v string) string { func InitializeConfigBundle(opts Options) (*bundle.Bundle, error) { genOptions := []generate.Option{} - if opts.TalosVersion != "" { - versionContract, err := config.ParseContractFromVersion(opts.TalosVersion) - if err != nil { - return nil, errors.Wrap(err, "invalid talos-version") - } - - genOptions = append(genOptions, generate.WithVersionContract(versionContract)) + versionContract, err := renderContract(opts.TalosVersion) + if err != nil { + return nil, err } + // Safe to pass unconditionally: an unset version resolves to the typed nil + // machinery uses for "current", which is also what the generator defaults to. + genOptions = append(genOptions, generate.WithVersionContract(versionContract)) + if opts.WithSecrets != "" { secretsBundle, err := secrets.LoadBundle(opts.WithSecrets) if err != nil { @@ -255,13 +272,31 @@ func InitializeConfigBundle(opts Options) (*bundle.Bundle, error) { } // SerializeConfiguration serializes the configuration bundle for machineType. -func SerializeConfiguration(configBundle *bundle.Bundle, machineType machine.Type) ([]byte, error) { - out, err := configBundle.Serialize(encoder.CommentsDisabled, machineType) +// +// talosVersion and kubernetesVersion must be the ones the bundle was built with. +// They decide whether the component images the generator produced are kept, and +// passing a different pair silently strips images out of documents that require +// one, yielding non-empty bytes the node then rejects. +func SerializeConfiguration(configBundle *bundle.Bundle, machineType machine.Type, talosVersion, kubernetesVersion string) ([]byte, error) { + versionContract, err := renderContract(talosVersion) if err != nil { - return nil, errors.Wrap(err, "serializing config bundle") + return nil, err + } + + // The direct-patch apply path comes through here rather than through the + // render, so it needs both the warning and the conflict check of its own. + warnUnpinnedKubernetesVersion(os.Stderr, versionContract, kubernetesVersion) + + serialized, err := serializeBundle(configBundle, machineType, versionContract, kubernetesVersion) + if err != nil { + return nil, err + } + + if err := checkSupersededFields(serialized, talosVersion); err != nil { + return nil, err } - return out, nil + return serialized, nil } // MergeFileAsPatch overlays the YAML body of patchFile onto rendered using @@ -1421,6 +1456,10 @@ const ( // cosiKindList is the COSI Kind value emitted when newLookupFunction // wraps multi-item lookups into a List envelope for template iteration. cosiKindList = "List" + // imageKey is the document key holding a Kubernetes component's image, both + // in v1alpha1 and in the typed documents that replaced it. + imageKey = "image" + // k8sKeyAPIVersion is the standard Kubernetes/COSI document key // used as part of the (apiVersion, kind, name) identity tuple. k8sKeyAPIVersion = "apiVersion" @@ -1876,15 +1915,15 @@ func applyPatchesAndRenderConfig(opts Options, configPatches []string) ([]byte, // Generate options for the configuration based on the provided flags genOptions := []generate.Option{} - if opts.TalosVersion != "" { - versionContract, err := config.ParseContractFromVersion(opts.TalosVersion) - if err != nil { - return nil, errors.Wrap(err, "invalid talos-version") - } - - genOptions = append(genOptions, generate.WithVersionContract(versionContract)) + versionContract, err := renderContract(opts.TalosVersion) + if err != nil { + return nil, err } + // Safe to pass unconditionally: an unset version resolves to the typed nil + // machinery uses for "current", which is also what the generator defaults to. + genOptions = append(genOptions, generate.WithVersionContract(versionContract)) + if opts.WithSecrets != "" { secretsBundle, err := secrets.LoadBundle(opts.WithSecrets) if err != nil { @@ -1960,7 +1999,7 @@ func applyPatchesAndRenderConfig(opts Options, configPatches []string) ([]byte, var configOrigin, configFull []byte if !opts.Full { - configOrigin, err = configBundle.Serialize(encoder.CommentsDisabled, machineType) + configOrigin, err = serializeBundle(configBundle, machineType, versionContract, opts.KubernetesVersion) if err != nil { return nil, errors.Wrap(err, "serializing original config bundle") } @@ -2006,11 +2045,31 @@ func applyPatchesAndRenderConfig(opts Options, configPatches []string) ([]byte, return nil, errors.Wrap(err, "applying patches to reloaded bundle") } - configFull, err = configBundle.Serialize(encoder.CommentsDisabled, machineType) + warnUnpinnedKubernetesVersion(os.Stderr, versionContract, opts.KubernetesVersion) + + configFull, err = serializeBundle(configBundle, machineType, versionContract, opts.KubernetesVersion) if err != nil { return nil, errors.Wrap(err, "serializing patched config bundle") } + // Validated on the bundle assembled with the chart's own documents, which is + // what an apply sends. The bundle alone would miss a chart that emits a typed + // document while leaving the v1alpha1 field in place — the half-migrated + // state the hint below invites — and a plain concatenation would carry the + // bundle's copy of that document alongside it, which machinery rejects as a + // duplicate before the conflict check is ever reached. + // + // A non-full render returns a patch rather than a config, so what it returns + // is deliberately not what gets checked here. + assembled, err := assembleTargetDocuments(configFull, nil, extraDocs) + if err != nil { + return nil, err + } + + if err := checkSupersededFields(assembled.Bytes(), opts.TalosVersion); err != nil { + return nil, err + } + var target []byte if opts.Full { target = configFull @@ -2029,6 +2088,91 @@ func applyPatchesAndRenderConfig(opts Options, configPatches []string) ([]byte, return buf.Bytes(), nil } +// checkSupersededFields fails the render when the config sets a v1alpha1 field +// that the targeted contract has moved into a document of its own. Talos rejects +// such a config outright, so catching it here turns an apply-time rejection into +// a render-time error naming the way out. +// +// The check asks each document directly rather than matching validation text: +// machinery exposes V1Alpha1ConflictValidate on every document that supersedes a +// v1alpha1 field, which is the same call its own container validation makes. +// Reading the messages instead would mean enumerating a dozen phrasings and +// silently missing whichever one gets added next. +// +// Only that class is reported: a render is legitimately incomplete in other ways +// (no install disk yet, no endpoint), and those are the operator's to fill in. +func checkSupersededFields(rendered []byte, talosVersion string) error { + cfg, err := configloader.NewFromBytes(rendered) + if err != nil { + // Not this check's job to decide the config is unloadable; the render + // carries on and the node reports whatever is wrong. The cost is that a + // chart document machinery cannot parse — an unregistered kind, say — + // takes this check down with it, so say so rather than vanish. + // The error is reported, so the return is not silent; it is just not + // this check's to fail on. + fmt.Fprintf(os.Stderr, "warning: could not load the rendered config for the v1alpha1 conflict check: %v\n", err) + + return nil + } + + legacy := cfg.RawV1Alpha1() + if legacy == nil { + return nil + } + + var conflicts []error + + for _, doc := range cfg.Documents() { + validator, supersedes := doc.(container.V1Alpha1ConflictValidator) + if !supersedes { + continue + } + + if err := validator.V1Alpha1ConflictValidate(legacy); err != nil { + conflicts = append(conflicts, err) + } + } + + if len(conflicts) == 0 { + return nil + } + + pinned := talosVersion + if pinned == "" { + pinned = "unset, so the render targets the Talos version talm was built from" + } + + conflict := errors.Wrap( + errors.Join(conflicts...), + "rendered config mixes v1alpha1 fields with the documents that superseded them", + ) + + // Above the boundary the contract is what moved the settings, so lowering it + // resolves the conflict. At or below it the contract is innocent: something + // in the charts or the patches wrote both shapes, and telling the operator + // to pin lower sends them somewhere they already are. + versionContract, err := renderContract(talosVersion) + if err != nil || !versionContract.MultidocKubernetesConfigSupported() { + //nolint:wrapcheck // cockroachdb/errors.WithHint at boundary. + return errors.WithHintf( + conflict, + "templateOptions.talosVersion is %s, which still keeps these settings in v1alpha1. "+ + "Something in the charts or the patches writes both shapes: drop the v1alpha1 field "+ + "the reported document supersedes.", + pinned, + ) + } + + //nolint:wrapcheck // cockroachdb/errors.WithHint at boundary. + return errors.WithHintf( + conflict, + "templateOptions.talosVersion is %s. Pin it to %s or lower in Chart.yaml; above that "+ + "contract Talos keeps these settings in documents of their own, which the charts do not emit.", + pinned, + config.TalosVersion1_13, + ) +} + // assembleTargetDocuments turns the serialized target into the bytes the render // returns: every document preserved in order, with the operator's comments // carried over onto the v1alpha1 one, followed by the chart's own documents. @@ -2053,16 +2197,8 @@ func assembleTargetDocuments(target []byte, talosPatches, extraDocs []string) (* return nil, errors.New("rendered config is empty") } - for _, configPatch := range talosPatches { - var sourceNode yaml.Node - - if err := yaml.Unmarshal([]byte(configPatch), &sourceNode); err != nil { - return nil, errors.Wrap(err, "unmarshaling source patch for comment propagation") - } - - dstPaths := make(map[string]*yaml.Node) - yamltools.CopyComments(&sourceNode, targetDocs[0], "", dstPaths) - yamltools.ApplyComments(targetDocs[0], "", dstPaths) + if err := propagatePatchComments(targetDocs[0], talosPatches); err != nil { + return nil, err } extraIdentities, err := documentIdentities(extraDocs) @@ -2107,6 +2243,24 @@ func assembleTargetDocuments(target []byte, talosPatches, extraDocs []string) (* return buf, nil } +// propagatePatchComments carries the operator's comments from the patches onto +// the rendered v1alpha1 document. +func propagatePatchComments(doc *yaml.Node, talosPatches []string) error { + for _, configPatch := range talosPatches { + var sourceNode yaml.Node + + if err := yaml.Unmarshal([]byte(configPatch), &sourceNode); err != nil { + return errors.Wrap(err, "unmarshaling source patch for comment propagation") + } + + dstPaths := make(map[string]*yaml.Node) + yamltools.CopyComments(&sourceNode, doc, "", dstPaths) + yamltools.ApplyComments(doc, "", dstPaths) + } + + return nil +} + // documentIdentities indexes raw YAML documents by apiVersion/kind/name. func documentIdentities(docs []string) (map[string]struct{}, error) { identities := make(map[string]struct{}, len(docs)) @@ -2138,9 +2292,180 @@ func documentIdentities(docs []string) (map[string]struct{}, error) { return identities, nil } +// mappingValue returns the value node for key in a mapping node, or nil. +func mappingValue(mapping *yaml.Node, key string) *yaml.Node { + if mapping == nil || mapping.Kind != yaml.MappingNode { + return nil + } + + for i := 0; i+1 < len(mapping.Content); i += 2 { + if mapping.Content[i].Value == key { + return mapping.Content[i+1] + } + } + + return nil +} + +// dropMappingKey removes key from a mapping node, if present. +func dropMappingKey(mapping *yaml.Node, key string) { + if mapping == nil || mapping.Kind != yaml.MappingNode { + return + } + + for i := 0; i+1 < len(mapping.Content); i += 2 { + if mapping.Content[i].Value == key { + mapping.Content = append(mapping.Content[:i], mapping.Content[i+2:]...) + + return + } + } +} + +// v1alpha1ComponentImages maps the v1alpha1 cluster component to the repository +// the generator defaults its image to. +// +//nolint:gochecknoglobals // immutable lookup used by stripDefaultedImages. +var v1alpha1ComponentImages = map[string]string{ + "apiServer": constants.KubernetesAPIServerImage, + "controllerManager": constants.KubernetesControllerManagerImage, + "proxy": constants.KubeProxyImage, + "scheduler": constants.KubernetesSchedulerImage, +} + +// defaultedImage reports whether value is exactly what the empty-version +// fallback would have produced for repository. +func defaultedImage(value, repository string) bool { + return value == fmt.Sprintf("%s:v%s", repository, kubeVersion("")) +} + +// dropDefaultedImage removes mapping's image key only when it still carries the +// value the fallback generated. An operator who wrote their own image — a mirror +// for an airgapped registry, a pinned build — keeps it, unless they wrote the +// exact string the fallback produces, which is indistinguishable from it. +func dropDefaultedImage(mapping *yaml.Node, repository string) { + image := mappingValue(mapping, imageKey) + if image == nil || !defaultedImage(image.Value, repository) { + return + } + + dropMappingKey(mapping, imageKey) +} + +// stripDefaultedImages removes the generator's own component images from a +// config whose project pinned no kubernetesVersion. An image the operator wrote +// is left alone. +// +// v1.14 refuses to generate without a version, and the fallback that satisfies +// it would write this binary's Kubernetes version into the config — moving a +// cluster to a version the operator never chose, invisibly, since the render's +// diff drops fields equal to the bundle default. +// +// This is a new contract rather than a restored one. Machinery before v1.14 +// emitted no image for an unset version, but talm never passed one through: it +// substituted its own built-in default before the value reached machinery, so an +// unpinned project used to get that. Removing the images means an unpinned +// project now follows each node's Talos release instead — which is a change in +// its own right, and why the render says so. +// +// It runs on the serialized stream rather than the bundle so it reaches the +// v1alpha1 document wherever the generator placed it. +func stripDefaultedImages(serialized []byte) ([]byte, error) { + docs, err := decodeYAMLDocuments(serialized) + if err != nil { + return nil, err + } + + for _, doc := range docs { + if len(doc.Content) == 0 { + continue + } + + root := doc.Content[0] + + // A typed document only reaches here on a contract that keeps the + // Kubernetes settings in v1alpha1, where it carries no component image. + // The contract that moved them is refused upstream of this call. + if mappingValue(root, k8sKeyKind) != nil { + continue + } + + dropDefaultedImage(mappingValue(mappingValue(root, "machine"), "kubelet"), constants.KubeletImage) + + cluster := mappingValue(root, "cluster") + for component, repository := range v1alpha1ComponentImages { + dropDefaultedImage(mappingValue(cluster, component), repository) + } + } + + buf := &bytes.Buffer{} + + for i, doc := range docs { + if i > 0 { + buf.WriteString("---\n") + } + + if err := encodeYAMLNodeIndented(buf, doc); err != nil { + return nil, err + } + } + + return buf.Bytes(), nil +} + +// serializeBundle serializes the bundle for machineType, dropping the component +// images when the project pinned no Kubernetes version and the contract still +// lets the node choose one. +func serializeBundle( + configBundle *bundle.Bundle, + machineType machine.Type, + versionContract *config.VersionContract, + kubernetesVersion string, +) ([]byte, error) { + out, err := configBundle.Serialize(encoder.CommentsDisabled, machineType) + if err != nil { + return nil, errors.Wrap(err, "serializing config bundle") + } + + if kubernetesVersion != "" { + return out, nil + } + + // From the contract that moved the Kubernetes settings into documents of + // their own, those documents require the image: machinery rejects an empty + // one, and there is no node-side default left to fall back to. Stripping + // there would produce a config the node refuses, so the version has to be + // pinned instead. + if versionContract.MultidocKubernetesConfigSupported() { + //nolint:wrapcheck // cockroachdb/errors.WithHint at boundary. + return nil, errors.WithHint( + errors.New("templateOptions.kubernetesVersion is not set"), + "this Talos contract keeps the Kubernetes settings in their own documents, which require an image. "+ + "Pin templateOptions.kubernetesVersion in Chart.yaml to the version your cluster runs.", + ) + } + + return stripDefaultedImages(out) +} + +// warnUnpinnedKubernetesVersion says out loud that the component versions now +// follow each node's own Talos release rather than a value in the project. It +// belongs to the render rather than to serializeBundle, which runs twice on the +// default path and would say it twice. +func warnUnpinnedKubernetesVersion(w io.Writer, versionContract *config.VersionContract, kubernetesVersion string) { + if kubernetesVersion != "" || versionContract.MultidocKubernetesConfigSupported() { + return + } + + fmt.Fprintln(w, + "warning: templateOptions.kubernetesVersion is not set, so no component images are pinned "+ + "and each node picks the Kubernetes version of the Talos release it runs. "+ + "Pin the key in Chart.yaml to choose it yourself.") +} + // decodeYAMLDocuments splits a YAML stream into one node per document, -// preserving order. Documents that hold nothing (a trailing separator, a -// comment-only chunk) are dropped rather than re-emitted as empty ones. +// preserving order. A chunk the decoder returns with no content — a +// comment-only document — is dropped rather than re-emitted as an empty one. func decodeYAMLDocuments(data []byte) ([]*yaml.Node, error) { dec := yaml.NewDecoder(bytes.NewReader(data)) @@ -2156,7 +2481,7 @@ func decodeYAMLDocuments(data []byte) ([]*yaml.Node, error) { } if err != nil { - return nil, errors.Wrap(err, "unmarshaling target config") + return nil, errors.Wrap(err, "decoding YAML documents") } if len(doc.Content) == 0 { diff --git a/pkg/engine/golden_test.go b/pkg/engine/golden_test.go index 0eb2cf83..af6afe86 100644 --- a/pkg/engine/golden_test.go +++ b/pkg/engine/golden_test.go @@ -63,6 +63,10 @@ func TestGoldenRender(t *testing.T) { {"generic-worker-multidoc", genericChartPath, "templates/worker.yaml", "v1.12"}, {"generic-controlplane-legacy", genericChartPath, "templates/controlplane.yaml", "v1.11"}, {"generic-worker-legacy", genericChartPath, "templates/worker.yaml", "v1.11"}, + // v1.13 is what charts/generic pins, so it is the contract that actually + // reaches users of that preset. + {"generic-controlplane-v113", genericChartPath, "templates/controlplane.yaml", "v1.13"}, + {"generic-worker-v113", genericChartPath, "templates/worker.yaml", "v1.13"}, } for _, tc := range cases { diff --git a/pkg/engine/talos_helpers.go b/pkg/engine/talos_helpers.go index 422a8db6..c4fec063 100644 --- a/pkg/engine/talos_helpers.go +++ b/pkg/engine/talos_helpers.go @@ -53,8 +53,9 @@ func failIfMultiNodes(ctx context.Context, command string) error { // The per-node loop is not unit-tested: resolving the kind goes through // client.ResolveResourceKind, which is a concrete method on the Talos client // and cannot be substituted. walkNodeResources below carries the per-node body -// and is tested directly; the loop itself is covered by the multi-node lookup -// case in the manual test plan. +// and is tested directly. The loop cannot run more than one iteration from talm +// anyway: Render calls failIfMultiNodes before installing the lookup function, +// so a render is always single-node. // // The kind is resolved once, against the first node: a resource definition is // cluster-wide, so which node answers does not matter. A per-node lookup failure diff --git a/pkg/engine/testdata/golden/generic-controlplane-v113.golden.yaml b/pkg/engine/testdata/golden/generic-controlplane-v113.golden.yaml new file mode 100644 index 00000000..7804e873 --- /dev/null +++ b/pkg/engine/testdata/golden/generic-controlplane-v113.golden.yaml @@ -0,0 +1,49 @@ + +machine: + type: controlplane + kubelet: + nodeIP: + validSubnets: + - 192.168.1.0/24 + install: + + # -- Discovered disks: + disk: "/dev/sda" + +cluster: + network: + podSubnets: + - 10.244.0.0/16 + serviceSubnets: + - 10.96.0.0/16 + clusterName: "generic" + controlPlane: + endpoint: "https://talm-test.invalid:6443" + apiServer: + etcd: + advertisedSubnets: + - 192.168.1.0/24 +# -- Discovered interfaces: +# eth0: +# hardwareAddr:aa:bb:cc:00:00:01 +# busPath: pci-0000:00:1f.0 +# driver: +# vendor: +# product: ) +--- +apiVersion: v1alpha1 +kind: HostnameConfig +hostname: "talos-23c56" +--- +apiVersion: v1alpha1 +kind: ResolverConfig +nameservers: + - address: "8.8.8.8" +--- +apiVersion: v1alpha1 +kind: LinkConfig +name: eth0 +addresses: + - address: 192.168.201.10/24 +routes: + - gateway: 192.168.201.1 diff --git a/pkg/engine/testdata/golden/generic-worker-v113.golden.yaml b/pkg/engine/testdata/golden/generic-worker-v113.golden.yaml new file mode 100644 index 00000000..5c343080 --- /dev/null +++ b/pkg/engine/testdata/golden/generic-worker-v113.golden.yaml @@ -0,0 +1,45 @@ + +machine: + type: worker + kubelet: + nodeIP: + validSubnets: + - 192.168.1.0/24 + install: + + # -- Discovered disks: + disk: "/dev/sda" + +cluster: + network: + podSubnets: + - 10.244.0.0/16 + serviceSubnets: + - 10.96.0.0/16 + clusterName: "generic" + controlPlane: + endpoint: "https://talm-test.invalid:6443" +# -- Discovered interfaces: +# eth0: +# hardwareAddr:aa:bb:cc:00:00:01 +# busPath: pci-0000:00:1f.0 +# driver: +# vendor: +# product: ) +--- +apiVersion: v1alpha1 +kind: HostnameConfig +hostname: "talos-23c56" +--- +apiVersion: v1alpha1 +kind: ResolverConfig +nameservers: + - address: "8.8.8.8" +--- +apiVersion: v1alpha1 +kind: LinkConfig +name: eth0 +addresses: + - address: 192.168.201.10/24 +routes: + - gateway: 192.168.201.1 From e0ac8810d3737cc74a2a0d53a536257fea1342e5 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 11 Sep 2026 22:28:49 +0300 Subject: [PATCH 07/10] chore(license): keep the code ported from Talos under MPL-2.0 Talos is MPL-2.0 and talm is Apache-2.0. The bump carried two ports over that line: the resource walk and multi-node guard talosctl stopped exporting, and the client constructors v1.14 removed. Both are derived work, so the files holding them now carry the MPL notice. MPL-2.0 section 3.3 allows the combined work to ship under Apache-2.0 as long as those files keep their own terms. The client constructors sat in root.go next to talm's own code. They move to a file of their own so the notice covers the derived code and nothing else. WithClientNoNodes stays in root.go as the dispatcher that routes --skip-verify. Signed-off-by: Aleksei Sviridkin Assisted-by: LLM --- README.md | 4 + pkg/commands/client_wrappers_test.go | 3 + pkg/commands/root.go | 114 +---------------------- pkg/commands/skip_verify_test.go | 2 +- pkg/commands/talos_client.go | 132 +++++++++++++++++++++++++++ pkg/commands/talosctl_wrapper.go | 2 +- pkg/engine/talos_helpers.go | 30 +++--- 7 files changed, 162 insertions(+), 125 deletions(-) create mode 100644 pkg/commands/talos_client.go diff --git a/README.md b/README.md index 184e9f90..f412c490 100644 --- a/README.md +++ b/README.md @@ -60,3 +60,7 @@ Full documentation lives at **[talm.cozystack.io](https://talm.cozystack.io/)**. - [CLI reference](https://talm.cozystack.io/reference/) — commands and flags. The site is built from `docs/` in this repository; the CLI reference under `docs/reference/` is generated from the command tree. + +## License + +Apache-2.0, except for two files ported from [siderolabs/talos](https://github.com/siderolabs/talos), which is MPL-2.0: `pkg/engine/talos_helpers.go` and `pkg/commands/talos_client.go` carry Talos code that v1.14 stopped exporting, so they stay under MPL-2.0 and say so in their headers. MPL-2.0 section 3.3 covers distributing the combined work under Apache-2.0. diff --git a/pkg/commands/client_wrappers_test.go b/pkg/commands/client_wrappers_test.go index b1150d1d..079aa786 100644 --- a/pkg/commands/client_wrappers_test.go +++ b/pkg/commands/client_wrappers_test.go @@ -141,6 +141,9 @@ func TestWithClientNoNodes_UsesContextEndpointsWithoutNodes(t *testing.T) { // contract: a signal cancels the context the client call runs under. The second // half (a second Ctrl+C killing the process) follows from unregistering the // handler, which Go's default disposition then handles. +// +// The raise is process-scope, so this test must not run in parallel and the +// package must not gain a second test that installs its own SIGTERM handler. func TestSignalContext_CancelsOnSignal(t *testing.T) { ctx, stop := signalContext() defer stop() diff --git a/pkg/commands/root.go b/pkg/commands/root.go index 395e8de7..d60655fd 100644 --- a/pkg/commands/root.go +++ b/pkg/commands/root.go @@ -18,11 +18,8 @@ import ( "context" "crypto/tls" "encoding/base64" - "fmt" "os" - "os/signal" "path/filepath" - "syscall" "time" "github.com/cockroachdb/errors" @@ -53,38 +50,6 @@ var SkipVerify bool // talosconfig. var errContextNotFound = errors.New("context not found in talosconfig") -// signalContext returns a context cancelled on SIGINT/SIGTERM, mirroring the -// wrappers talosctl builds its own clients with. -// -// It unregisters the handler on the first signal, so a second Ctrl+C kills the -// process outright. signal.NotifyContext would keep the registration, and the -// second signal would land in a full channel and be discarded, leaving a stuck -// call with no way out from the keyboard. -// -// Follows siderolabs/talos pkg/cli/context.go, which is licensed under MPL-2.0: -// https://github.com/siderolabs/talos/blob/v1.14.0/pkg/cli/context.go -func signalContext() (context.Context, context.CancelFunc) { - ctx, cancel := context.WithCancel(context.Background()) - - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) - - go func() { - select { - case <-sigCh: - signal.Stop(sigCh) - fmt.Fprintln(os.Stderr, "Signal received, aborting, press Ctrl+C once again to abort immediately...") - cancel() - case <-ctx.Done(): - } - }() - - return ctx, func() { - signal.Stop(sigCh) - cancel() - } -} - // skipVerifyTLSConfig builds a TLS config that skips server-certificate // verification while preserving client-certificate authentication taken from the // talosconfig context. @@ -181,48 +146,7 @@ func WithClientNoNodes(action func(context.Context, *client.Client) error, dialO return WithClientSkipVerify(action, dialOptions...) } - ctx, stop := signalContext() - defer stop() - - // Built on pkg/machinery/client rather than the talosctl wrapper: Talos - // v1.14 replaced that wrapper with a ClientFactory which refuses to - // construct without nodes, which is the one thing this function allows. - // - // The option set follows siderolabs/talos - // cmd/talosctl/pkg/talos/global/client.go (MPL-2.0): - // https://github.com/siderolabs/talos/blob/v1.13.7/cmd/talosctl/pkg/talos/global/client.go - cfg, err := clientconfig.Open(GlobalArgs.Talosconfig) - if err != nil { - return errors.Wrapf(err, "opening talosconfig %q", GlobalArgs.Talosconfig) - } - - opts := []client.OptionFunc{ - client.WithConfig(cfg), - client.WithDefaultGRPCDialOptions(), - client.WithGRPCDialOptions(dialOptions...), - client.WithSideroV1KeysDir(clientconfig.CustomSideroV1KeysDirPath(GlobalArgs.SideroV1KeysDir)), - } - - if GlobalArgs.CmdContext != "" { - opts = append(opts, client.WithContextName(GlobalArgs.CmdContext)) - } - - if len(GlobalArgs.Endpoints) > 0 { - opts = append(opts, client.WithEndpoints(GlobalArgs.Endpoints...)) - } - - if GlobalArgs.Cluster != "" { - opts = append(opts, client.WithCluster(GlobalArgs.Cluster)) - } - - c, err := client.New(ctx, opts...) - if err != nil { - return errors.Wrap(err, "constructing Talos client") - } - - defer func() { _ = c.Close() }() - - return action(ctx, c) + return newClientNoNodes(action, dialOptions...) } // withNodesMetadata attaches the plural "nodes" key to the request context. @@ -263,41 +187,9 @@ func WithClient(action func(context.Context, *client.Client) error, dialOptions ) } -// WithClientMaintenance wraps common code to initialize Talos client in maintenance (insecure mode). -// -// One client spans every node in GlobalArgs.Nodes, as the talosctl wrapper did -// before v1.14 hid it behind a per-node ClientFactory; callers that need a -// single-endpoint client narrow the list themselves (openClientPerNodeMaintenance). -// GlobalArgs.Nodes is read synchronously because that narrowing restores the -// saved list as soon as action returns. -// -// Follows the same upstream file as WithClientNoNodes above (MPL-2.0). -func WithClientMaintenance(enforceFingerprints []string, action func(context.Context, *client.Client) error) error { - ctx, stop := signalContext() - defer stop() - - nodes := GlobalArgs.Nodes - - c, err := client.New(ctx, - client.WithDefaultGRPCDialOptions(), - // Taken for the insecure TLS config and the fingerprint pinning. Its node - // argument is inert here: options are applied in order, and WithEndpoints - // below overwrites the single endpoint it sets. - client.WithMaintenanceMode("", enforceFingerprints), - client.WithEndpoints(nodes...), - ) - if err != nil { - return errors.Wrap(err, "constructing maintenance client") - } - - defer func() { _ = c.Close() }() - - return action(ctx, c) -} - // skipVerifyClientOptions assembles the client options for a --skip-verify -// connection. It mirrors upstream global.Args.WithClientNoNodes so the skip -// path does not silently lose behavior the normal path has: it pins the +// connection. It mirrors newClientNoNodes so the skip path does not silently +// lose behavior the normal path has: it pins the // already-resolved config context (so client.GetConfigContext honors // --talosconfig / --context instead of falling back to the default config), // forwards caller dial options, threads the --cluster proxy override when set, diff --git a/pkg/commands/skip_verify_test.go b/pkg/commands/skip_verify_test.go index f4998f66..714e04e2 100644 --- a/pkg/commands/skip_verify_test.go +++ b/pkg/commands/skip_verify_test.go @@ -250,7 +250,7 @@ func withGlobalArgsReset(t *testing.T) { // TestSkipVerifyClientOptions_ClusterThreaded pins that --cluster is not lost on // the skip-verify path: setting GlobalArgs.Cluster adds exactly one option -// (client.WithCluster), matching upstream global.Args.WithClientNoNodes. The +// (client.WithCluster), matching newClientNoNodes. The // option slice is the only observable surface — client.Options fields are // unexported — so coverage is by option count. func TestSkipVerifyClientOptions_ClusterThreaded(t *testing.T) { diff --git a/pkg/commands/talos_client.go b/pkg/commands/talos_client.go new file mode 100644 index 00000000..e8afc22b --- /dev/null +++ b/pkg/commands/talos_client.go @@ -0,0 +1,132 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// The code in this file is derived from siderolabs/talos, which is licensed +// under MPL-2.0, and is therefore kept under MPL-2.0 itself. The rest of talm +// stays under Apache-2.0; MPL-2.0 §3.3 covers that combination. +// +// signalContext follows pkg/cli/context.go: +// https://github.com/siderolabs/talos/blob/v1.14.0/pkg/cli/context.go +// +// newClientNoNodes and WithClientMaintenance follow +// cmd/talosctl/pkg/talos/global/client.go, whose exported entry points v1.14 +// removed: +// https://github.com/siderolabs/talos/blob/v1.13.7/cmd/talosctl/pkg/talos/global/client.go + +package commands + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/cockroachdb/errors" + "github.com/siderolabs/talos/pkg/machinery/client" + clientconfig "github.com/siderolabs/talos/pkg/machinery/client/config" + "google.golang.org/grpc" +) + +// signalContext returns a context cancelled on SIGINT/SIGTERM, mirroring the +// wrappers talosctl builds its own clients with. +// +// It unregisters the handler on the first signal, so a second Ctrl+C kills the +// process outright. signal.NotifyContext would keep the registration, and the +// second signal would land in a full channel and be discarded, leaving a stuck +// call with no way out from the keyboard. +func signalContext() (context.Context, context.CancelFunc) { + ctx, cancel := context.WithCancel(context.Background()) + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) + + go func() { + select { + case <-sigCh: + signal.Stop(sigCh) + fmt.Fprintln(os.Stderr, "Signal received, aborting, press Ctrl+C once again to abort immediately...") + cancel() + case <-ctx.Done(): + } + }() + + return ctx, func() { + signal.Stop(sigCh) + cancel() + } +} + +// newClientNoNodes constructs a client that carries no node metadata and runs +// action against it. +func newClientNoNodes(action func(context.Context, *client.Client) error, dialOptions ...grpc.DialOption) error { + ctx, stop := signalContext() + defer stop() + + // Built on pkg/machinery/client rather than the talosctl wrapper: Talos + // v1.14 replaced that wrapper with a ClientFactory which refuses to + // construct without nodes, which is the one thing this function allows. + cfg, err := clientconfig.Open(GlobalArgs.Talosconfig) + if err != nil { + return errors.Wrapf(err, "opening talosconfig %q", GlobalArgs.Talosconfig) + } + + opts := []client.OptionFunc{ + client.WithConfig(cfg), + client.WithDefaultGRPCDialOptions(), + client.WithGRPCDialOptions(dialOptions...), + client.WithSideroV1KeysDir(clientconfig.CustomSideroV1KeysDirPath(GlobalArgs.SideroV1KeysDir)), + } + + if GlobalArgs.CmdContext != "" { + opts = append(opts, client.WithContextName(GlobalArgs.CmdContext)) + } + + if len(GlobalArgs.Endpoints) > 0 { + opts = append(opts, client.WithEndpoints(GlobalArgs.Endpoints...)) + } + + if GlobalArgs.Cluster != "" { + opts = append(opts, client.WithCluster(GlobalArgs.Cluster)) + } + + c, err := client.New(ctx, opts...) + if err != nil { + return errors.Wrap(err, "constructing Talos client") + } + + defer func() { _ = c.Close() }() + + return action(ctx, c) +} + +// WithClientMaintenance wraps common code to initialize Talos client in maintenance (insecure mode). +// +// One client spans every node in GlobalArgs.Nodes, as the talosctl wrapper did +// before v1.14 hid it behind a per-node ClientFactory; callers that need a +// single-endpoint client narrow the list themselves (openClientPerNodeMaintenance). +// GlobalArgs.Nodes is read synchronously because that narrowing restores the +// saved list as soon as action returns. +func WithClientMaintenance(enforceFingerprints []string, action func(context.Context, *client.Client) error) error { + ctx, stop := signalContext() + defer stop() + + nodes := GlobalArgs.Nodes + + c, err := client.New(ctx, + client.WithDefaultGRPCDialOptions(), + // Taken for the insecure TLS config and the fingerprint pinning. Its node + // argument is inert here: options are applied in order, and WithEndpoints + // below overwrites the single endpoint it sets. + client.WithMaintenanceMode("", enforceFingerprints), + client.WithEndpoints(nodes...), + ) + if err != nil { + return errors.Wrap(err, "constructing maintenance client") + } + + defer func() { _ = c.Close() }() + + return action(ctx, c) +} diff --git a/pkg/commands/talosctl_wrapper.go b/pkg/commands/talosctl_wrapper.go index d5fd81ba..83436a65 100644 --- a/pkg/commands/talosctl_wrapper.go +++ b/pkg/commands/talosctl_wrapper.go @@ -184,7 +184,7 @@ func republishContainerFlags(cmd, wrappedCmd *cobra.Command) { // warnSkipVerifyUnsupported writes a warning to w when --skip-verify is set for // a wrapped talosctl passthrough command. Those commands run upstream RunE code -// that builds its own client through upstream global.Args, which has no +// that builds its own client through upstream's ClientFactory, which has no // skip-verify concept — only the dropped cozystack/talos fork could inject it // at the library level. So --skip-verify is a no-op for them; the warning keeps // the user from chasing an opaque TLS SAN failure. talm-native commands (apply, diff --git a/pkg/engine/talos_helpers.go b/pkg/engine/talos_helpers.go index c4fec063..b510a62a 100644 --- a/pkg/engine/talos_helpers.go +++ b/pkg/engine/talos_helpers.go @@ -1,3 +1,21 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// The code in this file is derived from siderolabs/talos, which is licensed +// under MPL-2.0, and is therefore kept under MPL-2.0 itself. The rest of talm +// stays under Apache-2.0; MPL-2.0 §3.3 covers that combination. +// +// Talos v1.14.0 dropped FailIfMultiNodes and ForEachResource from +// cmd/talosctl/pkg/talos/helpers: talosctl inlined the resource loop into its +// own get command and left no exported replacement. The versions here follow +// resources.go (the resource walk) and checks.go (the multi-node guard) and +// behave as the helpers did, which is what the callers in engine.go expect: +// https://github.com/siderolabs/talos/tree/v1.13.7/cmd/talosctl/pkg/talos/helpers +// +// Errors here go through the stdlib rather than cockroachdb/errors as the rest +// of talm does, so the bodies stay diffable against upstream when it changes. + package engine import ( @@ -13,18 +31,6 @@ import ( "github.com/siderolabs/talos/pkg/machinery/client" ) -// Talos v1.14.0 dropped FailIfMultiNodes and ForEachResource from -// cmd/talosctl/pkg/talos/helpers: talosctl inlined the resource loop into its -// own get command and left no exported replacement. Both are thin wrappers over -// the public client API, so talm carries its own, the same way it carries -// --skip-verify since the fork was dropped. Behaviour matches what the helpers -// did before they were dropped, which is what the callers in engine.go expect. -// -// These follow the structure of siderolabs/talos -// cmd/talosctl/pkg/talos/helpers/resources.go (the resource walk) and -// checks.go (the multi-node guard), both licensed under MPL-2.0: -// https://github.com/siderolabs/talos/tree/v1.13.7/cmd/talosctl/pkg/talos/helpers - // ErrMultiNodeUnsupported is returned for a command that only makes sense // against a single node when the context names more than one. var ErrMultiNodeUnsupported = errors.New("command is not supported with multiple nodes") From e8f305fff6548c99f4fa4b24fb10eaaea0a85477 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Sat, 12 Sep 2026 02:01:25 +0300 Subject: [PATCH 08/10] test(commands): keep pkg/commands building on Windows The test raises SIGTERM at its own process to prove signalContext cancels on it, and syscall.Kill does not exist on Windows. One undefined symbol in a test file fails the whole package's type check, so nothing in pkg/commands built there: both the Windows test job and the Windows lint job went down on it while Linux stayed green. Move it behind a !windows build tag. signalContext itself is unaffected, since syscall.SIGTERM is defined on Windows as well; only delivering a signal this way is not. Signed-off-by: Aleksei Sviridkin Assisted-by: LLM --- pkg/commands/client_wrappers_test.go | 25 ------------ pkg/commands/signal_context_unix_test.go | 50 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 25 deletions(-) create mode 100644 pkg/commands/signal_context_unix_test.go diff --git a/pkg/commands/client_wrappers_test.go b/pkg/commands/client_wrappers_test.go index 079aa786..92c5c32d 100644 --- a/pkg/commands/client_wrappers_test.go +++ b/pkg/commands/client_wrappers_test.go @@ -16,12 +16,9 @@ package commands import ( "context" - "os" "path/filepath" "slices" - "syscall" "testing" - "time" "google.golang.org/grpc/metadata" @@ -136,25 +133,3 @@ func TestWithClientNoNodes_UsesContextEndpointsWithoutNodes(t *testing.T) { t.Error("WithClientNoNodes attached node metadata; callers add nodes themselves") } } - -// TestSignalContext_CancelsOnSignal pins the first half of the interrupt -// contract: a signal cancels the context the client call runs under. The second -// half (a second Ctrl+C killing the process) follows from unregistering the -// handler, which Go's default disposition then handles. -// -// The raise is process-scope, so this test must not run in parallel and the -// package must not gain a second test that installs its own SIGTERM handler. -func TestSignalContext_CancelsOnSignal(t *testing.T) { - ctx, stop := signalContext() - defer stop() - - if err := syscall.Kill(os.Getpid(), syscall.SIGTERM); err != nil { - t.Fatalf("raise SIGTERM: %v", err) - } - - select { - case <-ctx.Done(): - case <-time.After(5 * time.Second): - t.Fatal("context was not cancelled by SIGTERM") - } -} diff --git a/pkg/commands/signal_context_unix_test.go b/pkg/commands/signal_context_unix_test.go new file mode 100644 index 00000000..28886266 --- /dev/null +++ b/pkg/commands/signal_context_unix_test.go @@ -0,0 +1,50 @@ +// Copyright Cozystack Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !windows + +// Raising a signal at the running process needs syscall.Kill, which Windows +// does not have. signalContext itself builds everywhere: syscall.SIGTERM is +// defined on Windows too, it just cannot be delivered this way. + +package commands + +import ( + "os" + "syscall" + "testing" + "time" +) + +// TestSignalContext_CancelsOnSignal pins the first half of the interrupt +// contract: a signal cancels the context the client call runs under. The second +// half (a second Ctrl+C killing the process) follows from unregistering the +// handler, which Go's default disposition then handles. +// +// The raise is process-scope, so this test must not run in parallel and the +// package must not gain a second test that installs its own SIGTERM handler. +func TestSignalContext_CancelsOnSignal(t *testing.T) { + ctx, stop := signalContext() + defer stop() + + if err := syscall.Kill(os.Getpid(), syscall.SIGTERM); err != nil { + t.Fatalf("raise SIGTERM: %v", err) + } + + select { + case <-ctx.Done(): + case <-time.After(5 * time.Second): + t.Fatal("context was not cancelled by SIGTERM") + } +} From 3dff64c10cae65e16f7127b3caeb8197f71bdda8 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 18 Sep 2026 17:13:53 +0300 Subject: [PATCH 09/10] docs: name the insecure-apply flag correctly in the test plan The destructive-sections recap said `apply -I`. That spelling belongs to `template --in-place`; `apply` takes `-i` for the maintenance connection, as the rest of the plan already writes it. An operator following the recap would get an unknown-flag error and skip the check. Signed-off-by: Aleksei Sviridkin Assisted-by: LLM --- docs/manual-test-plan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manual-test-plan.md b/docs/manual-test-plan.md index 57edee60..524fa601 100644 --- a/docs/manual-test-plan.md +++ b/docs/manual-test-plan.md @@ -2103,7 +2103,7 @@ talm template -f nodes/node0.yaml || true ## Sanity-check block -Run after every destructive section (E, F, H, and anything that touches `--mode=auto` on a rebooting change / `--mode=staged` / `apply -I`): +Run after every destructive section (E, F, H, and anything that touches `--mode=auto` on a rebooting change / `--mode=staged` / `apply -i`): ```bash cd $PROJECT From c997ec41a88ead337af10520dafa417ec56c0fba Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 18 Sep 2026 17:35:55 +0300 Subject: [PATCH 10/10] docs: record where the default installer image moved on Talos v1.14 The upgrade flag's default is built from the image factory now, with the empty schematic pinned into the reference, where it used to be a plain ghcr.io tag. A mirror stocked only with ghcr.io will not serve it, and the pinned schematic fixes the extension set the default carries. The page already lists the other operator-visible v1.14 changes; this one was missing, and it is the one an airgapped operator runs into. Signed-off-by: Aleksei Sviridkin Assisted-by: LLM --- docs/operations/talosctl-commands.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/operations/talosctl-commands.md b/docs/operations/talosctl-commands.md index ac406870..5402dacc 100644 --- a/docs/operations/talosctl-commands.md +++ b/docs/operations/talosctl-commands.md @@ -49,6 +49,12 @@ Upstream dropped `--insecure` from `reset` in v1.14, so `talm reset --insecure` Upstream changed the default: `talm support` now encrypts the generated bundle with age, to a built-in list of Sidero Labs recipients. `--no-encryption` turns that off, `--encryption-recipients` sends it to recipients you choose, and `--encryption-no-default-recipients` keeps yours while dropping theirs. Worth knowing before collecting a bundle from a cluster whose contents you would rather not hand to a third party, encrypted or otherwise. +## `talm upgrade` — the default installer image moved to the image factory on Talos v1.14 + +Upstream builds the `--image` default from the image factory now: `factory.talos.dev/metal-installer/376567988ad…:v1.14.0`, where the digest is the factory's empty schematic. It used to be `ghcr.io/siderolabs/installer:`. Two things follow. The registry is different, so a mirror that only carries `ghcr.io` will not serve it. And the default now pins a schematic, so it ships the stock extension set rather than whatever a `ghcr.io` tag happened to hold. + +This only bites a bare `talm upgrade` with neither `-f` nor `--image`. With `-f`, talm resolves the target from `values.yaml::image` at the project root and never consults the upstream default, which is the flow the presets are built around. + ## `talm containers`, `logs`, `restart`, `stats` — `-k` / `--kubernetes` is deprecated Upstream replaced the flag with `--namespace`, which takes `system`, `cri` or `taloscontainers`. The old spelling still works and prints `Flag --kubernetes has been deprecated, use --namespace cri instead`, so scripts keep running for now; move them over before the flag goes the way of the others on this page.