Skip to content

fix(kubernetes): name TalosConfigTemplate by content hash - #3523

Open
mattia-eleuteri wants to merge 3 commits into
cozystack:mainfrom
mattia-eleuteri:fix/kubernetes-talosconfigtemplate-content-hash-name
Open

fix(kubernetes): name TalosConfigTemplate by content hash#3523
mattia-eleuteri wants to merge 3 commits into
cozystack:mainfrom
mattia-eleuteri:fix/kubernetes-talosconfigtemplate-content-hash-name

Conversation

@mattia-eleuteri

@mattia-eleuteri mattia-eleuteri commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Fixes #3515.

TalosConfigTemplate.spec is immutable — the vtalosconfigtemplate.cluster.x-k8s.io webhook denies any mutation with TalosConfigTemplate.Spec is immutable — but the talos-reconcile Job applied it under a fixed <release>-<group> name on every reconcile. Once the template existed, no change to the rendered worker machine config could ever reach the cluster: the apply was rejected server-side while the kubernetes-* HelmRelease stayed Ready. Reported with machine.network.searchDomains after a clusterDomain correction; the same held for certSANs, machine.registries and nameservers.

  • Name the TalosConfigTemplate <release>-<group>-<6-char hash of its spec>, mirroring the KubevirtMachineTemplate content-hash idiom already used in templates/cluster.yaml, and point MachineDeployment.spec.template.spec.bootstrap.configRef at that name. A content change now creates a new template instead of attempting a rejected mutation, and CAPI rolls the workers onto it.
  • Extract the machine config into a single named template (_talosconfigtemplate.tpl) shared by the two consumers that have to agree on the name byte-for-byte: the Job, which embeds it in the heredoc it kubectl applys, and the MachineDeployment, which hashes it into configRef. Rendering it in one place makes a name divergence — which would leave CAPI blocked on templates do not exist forever — structurally impossible. This also removes the duplicated kubelet-reservation computation that previously lived in the Job (net −274 lines in the tracked templates).
  • The runtime values the Job substitutes (${TALOS_CA_B64}, ${SVC_IP}, ${COREDNS_IP}, tokens) stay shell placeholders and are deliberately not part of the hash: they are facts of an already-provisioned cluster, not chart inputs, so a certificate rotation or a Service re-address does not roll workers.
  • Same fix applied to packages/apps/kubernetes-nodes, which carries the identical bug for split-out worker pools. tests/render-parity.sh compares the MachineDeployment across both charts, so the two charts must (and do) compute the same name for the same pool shape; make update now copies _talosconfigtemplate.tpl from the parent chart alongside versions.yaml / kubectl.tag.
  • Drive-by, required by the above: the Job now iterates the kubernetes.nodeGroups helper instead of .Values.nodeGroups directly, the same source cluster.yaml uses. Two different group sources could yield two different hashes, and it also means an install with an empty nodeGroups now gets a reconcile Job for the default md0 group (previously the chart rendered that group's MachineDeployment but no Job, so no template was ever created).

Upgrade impact — please read

Existing clusters roll their Talos workers once. The configRef moves from <release>-<group> to the hashed name; CAPI creates a new MachineSet for it and rolls machines onto it under the existing RollingUpdate strategy (maxSurge: maxReplicas, maxUnavailable: 1) plus the group's MachineHealthCheck. The rendered machine config itself is byte-identical to main for the same values (verified by diffing the rendered output), so this particular roll delivers no config change — it is the one-time cost of adopting the content-hash name, the same cost KubevirtMachineTemplate already paid. Every subsequent machine-config change then propagates instead of being silently dropped.

Between the Helm upgrade and the Job creating the new template, the MachineDeployment reports cannot create a new MachineSet when templates do not exist; existing workers are untouched during that window. The pre-existing fixed-name template is not deleted — the outgoing MachineSet still references it, and it is garbage-collected with the KamajiControlPlane when the cluster is deleted. Consequently hashed templates accumulate, one per distinct machine config, all owned by the KamajiControlPlane; nothing prunes them, since deleting a template a live MachineSet may still clone from would be unsafe.

The one roll is not the only one. Hashing the whole rendered spec means anything inside it becomes a roll trigger, including two things that are not edits to this cluster's Kubernetes resource. For a node group sized by instanceType, the kubelet reservations in the spec come from a live lookup of the VirtualMachineClusterInstancetype, so a later platform-side edit to that instancetype rotates the name and rolls every group referencing it. The built-in md0 group is exactly that shape (instanceType: u1.medium, empty resources). And since this branch merged main, talos.registryMirrors is inside the hashed spec too, so setting or changing a mirror now rolls the group rather than applying only to nodes created later (README.md said the opposite and is corrected in this PR). Both are the fix working as intended — a machine-config change reaching the workers is the whole point — but they are roll triggers that did not exist before, and the KubevirtMachineTemplate hash this mirrors has neither, because it embeds the instancetype by name and never its resolved size.

The roll is not free on a node group holding a scarce resource. From exercising this same rollout path on a production fleet of 13 tenant clusters, by rotating the KubevirtMachineTemplate hash on a GPU node group. maxSurge: maxReplicas is dangerous when the resource is constrained: with 6 GPUs and 1 free, CAPI created all 5 new machines at once and 3 sat Pending on Insufficient nvidia.com/..., each having already imported a 200 GiB disk. It converges, but it wastes a lot of import. The natural reflex, maxSurge: 0 with maxUnavailable: 1, deadlocks instead: CAPI computes maxScaledDown = available - (desired - maxUnavailable), which was 4 - 4 = 0, so it removed no old machine while the new ones waited for exactly those machines to release their GPUs; nothing moved until maxUnavailable was loosened to 2. And the MachineHealthCheck amplifies both — at maxUnhealthy: 100% and nodeStartupTimeout: 10m, machines queuing for a GPU were declared unhealthy and remediated in a loop, three deleted and recreated while perfectly healthy, each iteration re-importing the disk. The per-node-group nodeStartupTimeout override fixes that, but the default is shorter than a plausible queue wait. Anyone upgrading a fleet with a GPU pool hits all three, and this PR is what starts the roll.

Validation

  • helm unittest in packages/apps/kubernetes (209 tests) and packages/apps/kubernetes-nodes (15 tests) — pass, main's registry_mirrors_test.yaml and serial_console_log_test.yaml included. New suites pin the name agreement between the Job's TCT_NAME and the MachineDeployment's configRef as a shared literal, and pin that a machine-config change (GPU node labels) rotates it on both objects. The two golden Job-name hashes in talos_templates_test.yaml were updated (the Job spec changed).
  • hack/helm-unit-tests.sh (whole tree, includes kubernetes-nodes' render-parity.sh) — pass, GOLDEN PARITY: all pool objects byte-identical across all cases across the resources, gpu and kubelet-override cases, which is what proves both charts compute the same template name.
  • hack/cozytest.sh hack/talos-reconcile-heredoc_test.bats — pass, all five cases unmodified from main. This is the execution-level evidence that main's escaping survived the move into _talosconfigtemplate.tpl: it renders both charts with a hostile registryMirrors endpoint and hostile Talos image coordinates, runs the extracted heredoc through a real shell, and asserts the values come out literal rather than command-substituted. A rendered-string regex cannot catch a heredoc the shell refuses to emit; this does.
  • make generate in both packages — no diff. make rd-presets-check and make go-unit-tests — pass.
  • helm template diffed against main for the same values, rerun against current main after the merge since the earlier measurement was taken against a main that no longer exists: the only changes are the template name, the new TCT_NAME env var and comments; the machine config the Job applies is byte-identical on default values (92 lines), on a GPU node group (102) and with talos.registryMirrors set (97). The rendered Job command passes sh -n, and the heredoc it emits parses as valid YAML (including the nested data: machine config) after substituting the shell variables.
  • Multi-group render checked: per-group hashes are distinct, and flipping _cluster["cluster-domain"] rotates every group's name — the exact scenario from the issue that previously could not propagate.
  • Not verified: no live-cluster or e2e run (e2e.sh / chainsaw needs bare-metal nodes). The upgrade sequence above is derived from CAPI's documented MachineDeployment rollout behaviour and the chart's existing KubevirtMachineTemplate precedent, not observed end to end.

Release note

fix(kubernetes): name the worker TalosConfigTemplate after a hash of its content so machine-config changes (searchDomains, certSANs, registries, nameservers) actually propagate instead of being silently rejected by the immutability webhook. Upgrading rolls existing Talos workers once. On a node group holding a scarce resource such as GPUs, review the MachineDeployment surge settings and the MachineHealthCheck nodeStartupTimeout first: `maxSurge: 0` can deadlock the roll and the default startup timeout can trigger spurious remediation while machines queue for the resource.

Summary by CodeRabbit

  • New Features
    • Worker Talos configuration templates now receive content-based names, making configuration versions identifiable and immutable.
    • Bootstrap and reconciliation processes consistently use the matching configuration version.
    • Configuration changes, including GPU settings and registry mirror updates, now trigger updated worker configurations and node replacement when applicable.
  • Bug Fixes
    • Improved consistency between generated worker configuration and deployment references.
  • Tests
    • Added coverage for hashed names, shared references, and configuration changes.
  • Documentation
    • Clarified worker update behavior for air-gapped environments.

TalosConfigTemplate.spec is immutable — vtalosconfigtemplate.cluster.x-k8s.io
denies any mutation with "TalosConfigTemplate.Spec is immutable" — but the
talos-reconcile Job applied it under a fixed <release>-<group> name on every
reconcile. Once the template existed, no change to the rendered worker machine
config could reach the cluster: the apply was rejected server-side while the
HelmRelease stayed Ready. Observed with machine.network.searchDomains after a
clusterDomain correction; the same held for certSANs, machine.registries and
nameservers.

Name the template after a 6-char hash of its rendered spec, the idiom
KubevirtMachineTemplate already uses in the same chart, and point
MachineDeployment.spec.template.spec.bootstrap.configRef at that name. A content
change now creates a new template instead of attempting a rejected mutation, and
CAPI rolls the workers onto it.

The spec is rendered from one named template shared by the Job (which embeds it
in its heredoc) and the MachineDeployment (which hashes it into configRef), so
the two cannot compute different names. The runtime values the Job substitutes
(CA certificates, Service ClusterIP, tokens) stay shell placeholders and are not
hashed, so a certificate rotation does not roll workers.

Upgrade note: existing clusters roll their Talos workers once, because the
configRef moves from <release>-<group> to the hashed name and CAPI creates a new
MachineSet for it. The rendered machine config itself is unchanged by this
commit. The pre-existing fixed-name template is left in place — the outgoing
MachineSet still references it — and is garbage-collected with the
KamajiControlPlane when the cluster is deleted.

Fixes cozystack#3515

Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@dosubot dosubot Bot added area/kubernetes Issues or PRs related to the tenant Kubernetes app kind/bug Categorizes issue or PR as related to a bug labels Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3b798957-3e82-4fda-bc10-a31c10f0872b

📥 Commits

Reviewing files that changed from the base of the PR and between 7606c04 and 4206b9a.

📒 Files selected for processing (6)
  • packages/apps/kubernetes-nodes/templates/_talosconfigtemplate.tpl
  • packages/apps/kubernetes-nodes/templates/nodegroup.yaml
  • packages/apps/kubernetes/README.md
  • packages/apps/kubernetes/templates/cluster.yaml
  • packages/apps/kubernetes/templates/talos/_talosconfigtemplate.tpl
  • packages/apps/kubernetes/tests/talos_templates_test.yaml
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/apps/kubernetes-nodes/templates/nodegroup.yaml
  • packages/apps/kubernetes/templates/cluster.yaml
  • packages/apps/kubernetes/tests/talos_templates_test.yaml
  • packages/apps/kubernetes/templates/talos/_talosconfigtemplate.tpl
  • packages/apps/kubernetes-nodes/templates/_talosconfigtemplate.tpl

📝 Walkthrough

Walkthrough

Changes

Worker Talos specifications are now rendered by shared Helm helpers and hashed with six-character SHA-256 suffixes. MachineDeployments and reconcile Jobs use the same hashed TalosConfigTemplate name. Helm tests validate reference consistency and hash changes.

TalosConfigTemplate content hashing

Layer / File(s) Summary
Shared Talos worker specification
packages/apps/kubernetes*/templates/**, packages/apps/kubernetes-nodes/Makefile
Added synchronized helpers for rendering worker Talos specifications and computing six-character content hashes.
Hashed template wiring
packages/apps/kubernetes*/templates/**
MachineDeployment bootstrap references and reconcile Jobs now use the rendered specification and hashed TalosConfigTemplate name.
Hash reference validation
packages/apps/kubernetes*/tests/**
Tests validate hashed names, matching references, reconcile Job output, and GPU-driven hash changes.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Helm
  participant MachineDeployment
  participant TalosReconcileJob
  participant TalosConfigTemplate
  Helm->>MachineDeployment: Render hashed bootstrap configRef
  Helm->>TalosReconcileJob: Pass TCT_NAME and TCT_SPEC
  TalosReconcileJob->>TalosConfigTemplate: Apply hashed name and rendered spec
Loading

Possibly related PRs

Suggested labels: area/testing, backport

Suggested reviewers: ivanhunters, lexfrei

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: naming TalosConfigTemplate resources by rendered content hash.
Linked Issues check ✅ Passed The changes satisfy issue #3515 by hashing rendered specs and updating reconcile Job and MachineDeployment references to enable immutable-template rollouts.
Out of Scope Changes check ✅ Passed The changes remain within scope: shared templates, hashed references, tests, node-chart support, and related documentation address the stated objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Trivy (0.72.0)

Trivy execution failed: 2026-08-10T11:55:37Z FATAL Fatal error run error: fs scan error: scan error: scan failed: failed analysis: post analysis error: post analysis error: kubernetes scan error: fs filter error: fs filter error: walk error range error: stat smartylint.json: no such file or directory: range error: stat smartylint.json: no such file or directory


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the size/XXL This PR changes 1000+ lines, ignoring generated files label Aug 3, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/apps/kubernetes-nodes/templates/talos-reconcile-job.yaml (1)

233-253: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Plan for cleanup of superseded TalosConfigTemplates.

Each machine-config change now creates a new TalosConfigTemplate. The previous object stays, because the old MachineSet still references it, and the ownerReference targets the KamajiControlPlane. Nothing removes it after the rollout completes. Over the cluster lifetime these objects accumulate, one per machine-config change per node group.

Consider a follow-up that prunes templates which no MachineSet or Machine references anymore. Retention during the rollout is correct; unbounded retention is the part to address later.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/apps/kubernetes-nodes/templates/talos-reconcile-job.yaml` around
lines 233 - 253, Plan a follow-up cleanup for superseded TalosConfigTemplates
created by the reconciliation flow around the kubectl apply block. After rollout
completion, identify templates no longer referenced by any MachineSet or Machine
and delete only those unreferenced objects, preserving templates still needed
during an active rollout.
packages/apps/kubernetes-nodes/tests/talos_reconcile_job_test.yaml (1)

43-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the negative assertion that the parent chart suite has.

packages/apps/kubernetes/tests/talos_config_template_name_test.yaml pins a notMatchRegex guard against the old fixed name. This suite omits it. Add the same guard so a regression to a fixed name fails in both charts.

♻️ Proposed addition
       - matchRegex:
           path: spec.template.spec.containers[0].command[2]
           pattern: 'name: \$\{TCT_NAME\}'
+      - notMatchRegex:
+          path: spec.template.spec.containers[0].command[2]
+          pattern: 'name: \$\{RELEASE\}-\$\{GROUP_NAME\}'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/apps/kubernetes-nodes/tests/talos_reconcile_job_test.yaml` around
lines 43 - 49, Extend the assertions in the Talos reconcile job test alongside
the existing TCT_NAME checks by adding a notMatchRegex assertion for the old
fixed name, matching the guard used in talos_config_template_name_test.yaml.
Keep the current expected dynamic value and command regex assertions unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/apps/kubernetes-nodes/templates/_talosconfigtemplate.tpl`:
- Around line 41-48: Define the missing cozy-lib.resources.toFloat helper
locally for the kubernetes-nodes chart, or add a valid local cozy-lib dependency
that provides it. Update the existing call sites to use the available helper if
implementing it locally, and ensure Helm rendering no longer depends on an
undefined cozy-lib.resources.toFloat function.

---

Nitpick comments:
In `@packages/apps/kubernetes-nodes/templates/talos-reconcile-job.yaml`:
- Around line 233-253: Plan a follow-up cleanup for superseded
TalosConfigTemplates created by the reconciliation flow around the kubectl apply
block. After rollout completion, identify templates no longer referenced by any
MachineSet or Machine and delete only those unreferenced objects, preserving
templates still needed during an active rollout.

In `@packages/apps/kubernetes-nodes/tests/talos_reconcile_job_test.yaml`:
- Around line 43-49: Extend the assertions in the Talos reconcile job test
alongside the existing TCT_NAME checks by adding a notMatchRegex assertion for
the old fixed name, matching the guard used in
talos_config_template_name_test.yaml. Keep the current expected dynamic value
and command regex assertions unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4ce44873-041b-4da9-9fb8-9667e8adbd35

📥 Commits

Reviewing files that changed from the base of the PR and between f1f3836 and 7606c04.

📒 Files selected for processing (11)
  • packages/apps/kubernetes-nodes/Makefile
  • packages/apps/kubernetes-nodes/templates/_talosconfigtemplate.tpl
  • packages/apps/kubernetes-nodes/templates/nodegroup.yaml
  • packages/apps/kubernetes-nodes/templates/talos-reconcile-job.yaml
  • packages/apps/kubernetes-nodes/tests/nodegroup_test.yaml
  • packages/apps/kubernetes-nodes/tests/talos_reconcile_job_test.yaml
  • packages/apps/kubernetes/templates/cluster.yaml
  • packages/apps/kubernetes/templates/talos/_talosconfigtemplate.tpl
  • packages/apps/kubernetes/templates/talos/talos-reconcile-job.yaml
  • packages/apps/kubernetes/tests/talos_config_template_name_test.yaml
  • packages/apps/kubernetes/tests/talos_templates_test.yaml

Comment on lines +41 to +48
{{- define "kubernetes.talosConfigTemplateSpec" -}}
{{- $root := .root }}
{{- $group := .group }}
{{- $kubeletVersion := include "kubernetes.versionMap" $root | trim }}
{{- $talosVersion := $root.Values.talos.version }}
{{- $podCIDR := "10.243.0.0/16" }}
{{- $serviceCIDR := "10.95.0.0/16" }}
{{- $dnsDomain := include "kubernetes.tenantClusterDomain" $root }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm helper defines and talos values exist in the kubernetes-nodes chart.
set -uo pipefail

echo "== defines in kubernetes-nodes =="
rg -n --glob 'packages/apps/kubernetes-nodes/**' 'define "(kubernetes\.versionMap|kubernetes\.tenantClusterDomain|kubernetes\.cpuToMillicores|kubernetes\.nodeGroups)"'

echo "== cozy-lib dependency =="
fd -a 'Chart.yaml' packages/apps/kubernetes-nodes --exec cat

echo "== talos values =="
fd -a 'values.yaml' packages/apps/kubernetes-nodes -d 2 --exec rg -n -A5 '^talos:'

Repository: cozystack/cozystack

Length of output: 191


🏁 Script executed:

#!/bin/bash
# Inspect the kubernetes-nodes chart files and referenced helpers/values without relying on the prior failing command.
set -u

echo "== kubernetes-nodes tracked files =="
git ls-files packages/apps/kubernetes-nodes | sed -n '1,200p'

echo
echo "== Chart.yaml files under kubernetes-nodes =="
fd -a 'Chart\.yaml$' packages/apps/kubernetes-nodes -d 3 --exec sh -c 'echo "--- $1"; cat -n "$1"' sh {}

echo
echo "== values.yaml files under kubernetes-nodes with talos context =="
fd -a 'values\.yaml$' packages/apps/kubernetes-nodes -d 3 --exec sh -c 'echo "--- $1"; sed -n "1,260p" "$1" | nl -ba' sh {}

echo
echo "== template helper definitions under kubernetes-nodes =="
fd -a '^\._?.*\.tpl$' packages/apps/kubernetes-nodes -d 4 --exec sh -c '
echo "--- $1"
rg -n "define \"kubernetes\.(versionMap|tenantClusterDomain|cpuToMillicores|talosConfigTemplateSpec|talosConfigTemplateHash|nodeGroups)\"|cozy-lib\.resources|talos\." "$1" || true
' sh {}

Repository: cozystack/cozystack

Length of output: 2163


🏁 Script executed:

#!/bin/bash
set -u

echo "== Chart.yaml dependency section =="
sed -n '1,120p' packages/apps/kubernetes-nodes/Chart.yaml | cat -A

echo
echo "== talos section in values.yaml =="
python3 - <<'PY'
from pathlib import Path
p=Path('packages/apps/kubernetes-nodes/values.yaml')
lines=p.read_text().splitlines()
for i,l in enumerate(lines,1):
    if l.startswith('talos:'):
        start=max(1,i-3); end=min(i+50,len(lines))
        for j in range(start,end+1):
            print(f'{j:5d}: {lines[j-1]}')
        break
else:
    print('talos key not found')
PY

echo
echo "== value keys containing talos/version/schematic/installer =="
rg -n -i 'talos|installerRepository|schematicID|schematic|version' packages/apps/kubernetes-nodes/values.yaml packages/apps/kubernetes-nodes/templates packages/apps/kubernetes-nodes/tests || true

echo
echo "== helper definitions in packages/apps/kubernetes-nodes/templates =="
rg -n 'define "(kubernetes\.|cozy-lib\.|talos|talosConfigTemplateSpec|talosConfigTemplateHash)"' packages/apps/kubernetes-nodes/templates || true

Repository: cozystack/cozystack

Length of output: 22842


🏁 Script executed:

#!/bin/bash
set -u

echo "== talosConfig template reference and dependencies =="
python3 - <<'PY'
from pathlib import Path
p=Path('packages/apps/kubernetes-nodes/templates/_talosconfigtemplate.tpl')
s=p.read_text()
for name in ['kubernetes.versionMap','kubernetes.tenantClusterDomain','kubernetes.cpuToMillicores','cozy-lib.resources.toFloat','kubernetes.talosConfigTemplateHash']:
    print(f'{name}: {s.count(name)}')
PY

echo
echo "== tenantClusterDomain definitions/usages =="
rg -n 'define "kubernetes\.tenantClusterDomain"|kubernetes\.tenantClusterDomain|tenantClusterDomain' packages/apps/kubernetes-nodes/templates packages/apps/kubernetes-nodes/charts || true

echo
echo "== cpuToMillicores definitions/usages =="
rg -n 'define "kubernetes\.cpuToMillicores"|kubernetes\.cpuToMillicores|cpuToMillicores' packages/apps/kubernetes-nodes/templates packages/apps/kubernetes-nodes/charts || true

echo
echo "== resources.toFloat definitions/usages =="
rg -n 'define "cozy-lib\.resources\.toFloat|resources\.toFloat|toFloat' packages/apps/kubernetes-nodes/templates packages/apps/kubernetes-nodes/charts || true

echo
echo "== versionMap definition context =="
sed -n '1,80p' packages/apps/kubernetes-nodes/templates/_versions.tpl | cat -n

echo
echo "== Chart dependency files =="
find packages/apps/kubernetes-nodes -maxdepth 4 -type f \( -name 'Chart.yaml' -o -name 'Chart.lock' -o -name 'Chart.lock.yaml' \) -print | sort
find packages/apps/kubernetes-nodes -maxdepth 4 -type d | sort

Repository: cozystack/cozystack

Length of output: 5381


🏁 Script executed:

#!/bin/bash
set -u

echo "== kubernetes.tenantClusterDomain definition =="
sed -n '35,55p' packages/apps/kubernetes-nodes/templates/_helpers.tpl | cat -n

echo
echo "== kubernetes.cpuToMillicores definition =="
sed -n '1,25p' packages/apps/kubernetes-nodes/templates/_resources.tpl | cat -n

echo
echo "== cozy-lib resources.toFloat definition =="
sed -n '1,80p' packages/apps/kubernetes-nodes/charts/cozy-lib/templates/resources/_helpers.tpl 2>/dev/null | cat -n
find packages/apps/kubernetes-nodes/charts/cozy-lib -type f -name '*.tpl' -print | while read -r f; do
  if rg -q 'resources\.toFloat|define ".*toFloat|toFloat' "$f"; then
    echo "--- $f"
    rg -n 'define ".*toFloat|resources\.toFloat|toFloat' "$f" || true
  fi
done

echo
echo "== files/versions.yaml =="
sed -n '1,220p' packages/apps/kubernetes-nodes/files/versions.yaml | cat -n

echo
echo "== values talos keys =="
sed -n '88,100p' packages/apps/kubernetes-nodes/values.yaml | cat -n

Repository: cozystack/cozystack

Length of output: 3169


🏁 Script executed:

#!/bin/bash
set -u

echo "== find cozy-lib resource helpers/templates =="
git ls-files packages/apps/kubernetes-nodes/charts/cozy-lib | sed -n '1,120p'

echo
echo "== fuzzy search for resources/tofloat in kubernetes-nodes chart tree =="
rg -n -i 'resources|toFloat|define ".*toFloat|define ".*resource' packages/apps/kubernetes-nodes/charts packages/apps/kubernetes-nodes/templates packages/apps/kubernetes-nodes/values.yaml || true

echo
echo "== inspect Chart.yaml dependency/subchart declarations if any =="
python3 - <<'PY'
from pathlib import Path
p=Path('packages/apps/kubernetes-nodes/Chart.yaml')
s=p.read_text().splitlines()
for i,l in enumerate(s,1):
    if 'dependencies:' in l or 'dependencies' in l.lower():
        for j in range(max(1,i-3), min(len(s), i+30)+1):
            print(f'{j:5d}: {s[j-1]}')
        break
else:
    print('no dependencies block found')
PY

Repository: cozystack/cozystack

Length of output: 8653


Add the missing cozy-lib.resources.toFloat definition.

kubernetes-nodes includes cozy-lib.resources.toFloat, but the chart has no dependencies entry and packages/apps/kubernetes-nodes/charts/cozy-lib exists only as a directory here. Add a real local dependency/template that defines it, or replace this call with an equivalent local helper, otherwise Helm rendering can fail with function "cozy-lib.resources.toFloat" not defined.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/apps/kubernetes-nodes/templates/_talosconfigtemplate.tpl` around
lines 41 - 48, Define the missing cozy-lib.resources.toFloat helper locally for
the kubernetes-nodes chart, or add a valid local cozy-lib dependency that
provides it. Update the existing call sites to use the available helper if
implementing it locally, and ensure Helm rendering no longer depends on an
undefined cozy-lib.resources.toFloat function.

@mattia-eleuteri

Copy link
Copy Markdown
Collaborator Author

Note for reviewers: overlapping files with a sibling PR

packages/apps/kubernetes/templates/talos/talos-reconcile-job.yaml and
packages/apps/kubernetes/tests/talos_templates_test.yaml are also touched by #3521, which raises that Job's
kubectl container memory limit from 256Mi to 512Mi and exposes it as talos.reconcileJobMemoryLimit.

This PR restructures that Job substantially (the machine config moves into a shared named template), so #3521 will
need a rebase if this one lands first — or this one will, in the other order. No functional interaction: the memory
limit is not part of the hashed spec, so it does not affect the content-hash name. Happy to rebase either way.

@mattia-eleuteri

Copy link
Copy Markdown
Collaborator Author

Thanks, but this one is a false positive, and I want to record why so it does not cost a reviewer time.

packages/apps/kubernetes-nodes/charts/cozy-lib is not an empty directory, it is a symlink. On this branch:

$ git ls-tree HEAD packages/apps/kubernetes-nodes/charts/
120000 blob e18135098...    packages/apps/kubernetes-nodes/charts/cozy-lib

Mode 120000 is a symlink, and the target is the real library, where the helper is defined:

$ git grep -n 'define "cozy-lib.resources.toFloat"'
packages/library/cozy-lib/templates/_resources.tpl:42:{{- define "cozy-lib.resources.toFloat" -}}

So Helm resolves it at render time and there is nothing to add. Two further points confirm it: the call sites are not introduced by this PR (resources.toFloat is already used in nodegroup.yaml on main, so a missing definition would have been breaking kubernetes-nodes long before this change), and helm unittest passes on this branch, which it could not if the function were undefined, since the tests render the very templates that call it.

The analysis chain above shows the cause: the shell probes ran fd/rg inside the symlinked path and got no matches, which they reported as "exists only as a directory". Tooling that does not follow symlinks will keep reaching this conclusion on any Cozystack package, because every package vendors cozy-lib this way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOT LGTM. The fix is right and I verified it end to end, but the branch conflicts with main on the exact lines it rewrites, so it can't merge until it's rebased.

Blocker: rebase onto main

This branch is based on a commit that predates fix(kubernetes): render the talos-reconcile Job for the default md0 group on main. That commit switched the Job's group loop from .Values.nodeGroups to the kubernetes.nodeGroups helper, the same drive-by this PR describes, and it landed on the region this PR rewrites. Merging origin/main into the PR head conflicts in one file, packages/apps/kubernetes/templates/talos/talos-reconcile-job.yaml, and the conflicted region is the group loop plus the kubelet-reservation block this PR moves into _talosconfigtemplate.tpl.

Keep this branch's side of the conflict. It already iterates the helper, so the md0 fix is carried rather than reverted, and the rest of the file already references $tctSpec and $tctName. I resolved it that way locally and the chart suite is green with main's md0 regression test included: 195 tests against the 193 this branch has on its own.

Non-blocking

Hashing the whole rendered spec is the right call, but for a group sized by instanceType the kubelet reservations inside that spec come from a live lookup of the VirtualMachineClusterInstancetype (_talosconfigtemplate.tpl:74). The built-in md0 group is exactly that shape, instanceType: u1.medium with empty resources, so its name depends on an object the chart does not own. The upgrade note says workers roll once. A later platform-side change to that instancetype rotates the name and rolls them again. That is the fix working as intended, but it is a second roll trigger and it belongs in the upgrade note. The KubevirtMachineTemplate hash this mirrors does not have the property, since it embeds the instancetype by name and never its resolved size.

Nothing prunes superseded templates, as you say. templates/cluster.yaml:878 already enumerates live MachineSets to decide which KubevirtMachineTemplates to preserve across an upgrade, and the same enumeration gives a safe prune condition for hashed TalosConfigTemplates. Follow-up issue rather than more scope here.

The Job/MachineDeployment name agreement is pinned by two hand-maintained literals in tests/talos_config_template_name_test.yaml plus a comment saying they have to match. The guard is real: I diverged the hash input at one call site and the suite went red. But only one assertion of the pair fails, so the failure output does not say "these two must be equal", and re-pasting the new value is the obvious wrong fix. Comparing the Job's TCT_NAME against the MachineDeployment's configRef inside one render, the way tests/render-parity.sh already compares objects across charts, would make that self-evident.

What I checked

The machine config the Job applies is byte-identical to what main renders for the same values, plain and GPU node group both; only the template name, the new env var and comments differ. All four render sites agree on the name for the same pool shape: parent chart MachineDeployment and Job, split-pool chart MachineDeployment and Job. _talosconfigtemplate.tpl is byte-identical across the two charts and the split chart's update target copies it. helm lint fails on both charts with default values, identically on main, so no signal there.

No end-to-end coverage applies to this change. The split-pool chart is not exercised by the end-to-end suite at all, and the fork-PR path skips that suite anyway. The unit suites and the parity gate are the whole safety net, which argues for the third follow-up rather than against the change.

The bot note about cozy-lib.resources.toFloat being undefined in kubernetes-nodes is a false positive. packages/apps/kubernetes-nodes/charts/cozy-lib is a symlink to packages/library/cozy-lib, the helper resolves, and nodegroup.yaml on main already calls it a dozen times.

…ash-name

Resolves the conflict in packages/apps/kubernetes/templates/talos/talos-reconcile-job.yaml
by keeping this branch's side of the group loop.

The only upstream change to that file since the merge base is 4e9c5ce
"render the talos-reconcile Job for the default md0 group", which switches the
loop from .Values.nodeGroups to the kubernetes.nodeGroups helper. This branch
already iterates the helper, so keeping our side carries that fix rather than
reverting it; the machine config upstream still renders inline lives in
templates/talos/_talosconfigtemplate.tpl here, including $mgmtClusterDomain,
searchDomains/nameservers and the kubelet reservations, so nothing is lost.

Verified: 195 tests pass in packages/apps/kubernetes (main's md0 regression
test talos_reconcile_nodegroups_test.yaml included, 193 on this branch alone),
9 in packages/apps/kubernetes-nodes, and render-parity.sh reports GOLDEN
PARITY across the resources, gpu and kubelet cases. make generate is clean in
both packages.

Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
@mattia-eleuteri

Copy link
Copy Markdown
Collaborator Author

Rebased, thanks for the precise pointer. I merged upstream/main rather than rebasing so the review history stays intact and no force-push is needed. mergeable is back to MERGEABLE.

Conflict resolution. Kept this branch's side of the group loop, as you suggested. I checked why before doing it rather than taking it on faith: since the merge base, the only upstream change to talos-reconcile-job.yaml is 4e9c5ce, 8 insertions and 1 deletion in a single hunk, switching .Values.nodeGroups to the kubernetes.nodeGroups helper. This branch already iterates the helper, so keeping our side carries #3535 rather than reverting it. The rest of what upstream still renders inline lives in _talosconfigtemplate.tpl here, $mgmtClusterDomain and the searchDomains/nameservers block included, and $mgmtClusterDomain has no consumer left in the Job outside that region, so nothing is dropped.

Verification matches your local run: 195 tests in packages/apps/kubernetes with main's talos_reconcile_nodegroups_test.yaml included, 9 in packages/apps/kubernetes-nodes, and render-parity.sh reports GOLDEN PARITY: all pool objects byte-identical across all cases across the resources, gpu and kubelet cases. make generate clean in both packages.

On your non-blocking point about the lookup-derived hash. You are right and it is worth naming the consequence explicitly, because it follows from my choice to hash the whole rendered spec: for a group sized by instanceType, editing the VirtualMachineClusterInstancetype itself rotates the hash and rolls every worker of every group referencing it. That is a real behaviour change, and unlike a values edit it can happen without anyone touching the cluster's Kubernetes resource.

Two ways out, and I do not have a strong preference:

  • Exclude the kubelet-reservation block from the hash the way the runtime placeholders already are, on the grounds that it is derived state rather than a chart input. The cost is that a genuine reservation change then stops propagating, which is the bug this PR exists to fix, so it would need the reservations to move out of the immutable spec entirely.
  • Keep it in the hash and accept that a reservation change rolls workers, which is arguably correct, and document it.

One data point in favour of the current shape: under helm template without cluster access the parent chart fails loudly rather than computing a divergent hash, cluster.yaml:532 raises nodeGroup <name>: specified instanceType "<type>" not found in cluster. So the dry-run divergence you were worried about surfaces as an error, not as a silent name mismatch. That does not cover the in-cluster instancetype-edit case above.

Live evidence for the "Upgrade impact" section, which I flagged as unverified.

We now have it, from a production fleet of 13 tenant clusters. Last night we exercised the same rollout path this PR triggers, by changing a value that rotates the KubevirtMachineTemplate hash on a GPU node group (4 to 5 replicas plus diskSize 175Gi to 200Gi). Three things came out of it that the section should probably mention:

  1. maxSurge: maxReplicas is dangerous on a constrained resource. The fleet had 6 GPUs with 1 free. CAPI created all 5 new machines at once; 3 sat in Pending on Insufficient nvidia.com/GB202GL_RTX_PRO_6000_BLACKWELL_SERVER_EDITION, each having already pulled a 200 GiB image. It converges, but it is a lot of wasted import for nothing.

  2. maxSurge: 0 with maxUnavailable: 1 deadlocks in that same situation. CAPI computes maxScaledDown = available - (desired - maxUnavailable), which was 4 - 4 = 0, so it removed no old machine, while the new machines were waiting for exactly those machines to release their GPUs. Nothing moved until we loosened maxUnavailable to 2. Worth a sentence, because maxSurge: 0 is the natural reflex when capacity is tight and it is the one setting that cannot make progress.

  3. The MachineHealthCheck amplifies both. With maxUnhealthy: 100% and nodeStartupTimeout: 10m, the machines queuing for a GPU were declared unhealthy and remediated in a loop, three of them deleted and recreated while perfectly healthy, each iteration re-importing the disk. The per-node-group nodeStartupTimeout override fixes it, but the default is shorter than a plausible queue wait.

None of this changes the correctness of this PR, and I am not proposing to widen its scope. It does mean the one-time roll it ships is not free for clusters with a scarce-resource node group, so I would rather say so in the release note than have operators discover it.

Happy to add a paragraph to the upgrade-impact section covering the above if you want it in this PR, or to open it as a follow-up against the MachineDeployment strategy defaults.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOT LGTM. The 5 August merge fixed my blocker and I checked it, but main moved and the same two files conflict again. This time picking a side is the wrong resolution.

Blocker: the conflict now carries a security fix

Since your merge base, main landed 3a1292d68 (escape tenant values in the worker reconcile heredoc, #3513) and 58910c0ed (registry mirror passthrough). Both edit the machine-config block this PR moves into _talosconfigtemplate.tpl. main escapes backslash, dollar and backtick on talosVersion, installerRepository and schematicID, adds an escaped machine.registries.mirrors block, and puts an INVARIANT comment above data: saying why anything new there needs escaping or render-time validation.

The helper here has none of that. _talosconfigtemplate.tpl:115 and :195 interpolate raw, and the sink is untouched: talos-reconcile-job.yaml:336 is still cat <<EOF | kubectl apply -f - with the spec nindented in at :349, same at :238 and :251 in the split chart. Keep this branch's side and the injection is back, in both charts, with the mirror feature gone too.

So port both commits into _talosconfigtemplate.tpl rather than resolve by picking. The INVARIANT comment belongs there as well, since that file is where the next field gets added and it is the one place that says nothing about the heredoc.

Two things follow from the port. talos.registryMirrors ends up inside the hashed spec, so changing it rolls the workers instead of only affecting nodes created later, and packages/apps/kubernetes/README.md currently tells operators the opposite. And "byte-identical to main" was measured against a main that no longer exists, so it needs rerunning. I expect it still holds on default values: the escapes are no-ops on ordinary version strings and the mirrors block is gated on a non-empty map.

Last round

Blocker cleared. The Job ranges over kubernetes.nodeGroups at talos-reconcile-job.yaml:378, git diff df157da61..HEAD is exactly the eleven files this PR owns, and main's talos_reconcile_nodegroups_test.yaml is green. 195 tests in packages/apps/kubernetes, 9 in packages/apps/kubernetes-nodes, parity clean.

I agree with keeping the reservations in the hash. What is still missing is the consequence in "Upgrade impact": an instancetype edit rotates the name later, and the section only describes the one roll. Loud rather than silent, at least. With an instanceType group and no explicit resources, a missed lookup aborts the render at cluster.yaml:532, so a configRef that no Job will ever satisfy cannot be produced.

Pruning and the two literals in tests/talos_config_template_name_test.yaml:37,56 are unchanged. Both were follow-ups.

Hash behaviour

Rendered the chart against changed values to see what the hash tracks. Management cluster-domain rotates the name, 35293f to 4cad8d, which is the #3515 case. Memory rotates it through the reservations, gpus through nodeLabels. diskSize and maxReplicas leave it alone, and diskSize rotates the KubevirtMachineTemplate hash instead. Behaves as described.

Your rollout evidence

It supports the mechanism about as well as anything short of an e2e run can: a KubevirtMachineTemplate rotation and a bootstrap.configRef rotation both sit in MachineDeployment.spec.template.spec and take the same rollout path. Put the scarce-resource part into "Upgrade impact" instead of leaving it in a comment. Anyone upgrading a fleet with a GPU pool hits all three of those, and this PR is what starts the roll.

CI

"Build packages/apps/kubernetes" and "Build Talos" are red on denied: Anonymous users are only allowed read access on public repos at the image push. Fork path, not this change.

Merge order

Sequencing is my call, nothing to fix here. Merging ahead of #3571 switches a MachineDeployment from a hand-written TalosConfigTemplate to a rendered one, and a cluster running GPU workers on such a template loses its schematic when that happens, because the values surface to restore it exists only in #3571. #3571 first, or the two together.

…ash-name

main landed 3a1292d (escape tenant values in the worker reconcile
heredoc) and 58910c0 (registry mirror passthrough) on the machine-config
block this branch relocates into _talosconfigtemplate.tpl, so the conflict
is resolved by porting both commits into the helper rather than by picking
a side. Keeping this branch's side alone would have reopened the injection
in both charts and dropped the mirror feature.

Ported into the helper, in the parent chart and copied verbatim into
kubernetes-nodes so the two stay byte-identical:

  - talosVersion, installerRepository and schematicID take main's escape
    chain for backslash, dollar and backtick.
  - The `machine.registries.mirrors` block, escaped the same way.
  - The INVARIANT comment above `data:`. Reworded for its new home: the
    helper is where the next field gets added, the spec is nindented into
    the Job's heredoc by the caller rather than written there, and the
    comment now names the render-time-validated fields in both charts
    (cluster.yaml and nodegroup.yaml) because this file is shared.

The sink is unchanged; what moved is where the escaping lives.

Two consequences of the port, both real:

  - talos.registryMirrors is now inside the hashed spec, so changing it
    rotates the TalosConfigTemplate name and rolls the node group instead
    of applying only to nodes created later. Verified: the
    MachineDeployment configRef goes t-md0-35293f to t-md0-49ef3d on
    setting a mirror, and to t-md0-ef9cc5 on changing it. README.md said
    the opposite and is corrected.
  - The byte-identity claim was measured against a main that no longer
    exists, so it was rerun. The machine config this branch renders is
    byte-identical to what upstream/main renders for the same values on
    default values (92 lines), a GPU node group (102) and a set
    registryMirrors (97).

hack/talos-reconcile-heredoc_test.bats passes unmodified, all five cases
including the hostile-registryMirrors and hostile-image-coordinates ones,
which is the execution-level evidence that the escaping survived the move.
209 tests in packages/apps/kubernetes, 15 in packages/apps/kubernetes-nodes,
GOLDEN PARITY clean, make generate leaves no drift.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
@mattia-eleuteri

Copy link
Copy Markdown
Collaborator Author

Blocker addressed. Both commits are ported into _talosconfigtemplate.tpl rather than the conflict being resolved by picking a side.

In the helper, in the parent chart and copied verbatim into kubernetes-nodes so the two stay byte-identical: talosVersion, installerRepository and schematicID take the escape chain for backslash, dollar and backtick; the machine.registries.mirrors block comes across escaped the same way; and the INVARIANT comment moves above data: there. I reworded it for its new home rather than copying it verbatim, on two counts. The spec is rendered in the helper and nindented into the Job's heredoc by the caller, so the comment now says the invariant travels with the text rather than with the Job template. And because the file is shared between the two charts it names both render-time-validated sites, cluster.yaml in kubernetes and nodegroup.yaml in kubernetes-nodes, instead of one. The sink itself is untouched; what moved is where the escaping lives.

The execution-level evidence is that hack/talos-reconcile-heredoc_test.bats passes unmodified, all five cases, including the hostile registryMirrors endpoint and the hostile image coordinates, in both charts. That suite renders, extracts the heredoc and runs it through a real shell, so it is the check that a rendered-string regex could not do, and it did not need adjusting for the relocation.

You were right on both consequences.

talos.registryMirrors does land inside the hashed spec. Confirmed by rendering: the MachineDeployment configRef goes t-md0-35293f to t-md0-49ef3d on setting a mirror and to t-md0-ef9cc5 on changing it, so a mirror change now rolls the group rather than applying only to nodes created later. packages/apps/kubernetes/README.md claimed the opposite in the Phase 1 Talos rollover bullet and is corrected.

And the byte-identity claim needed rerunning, since it was measured against a main that no longer exists. It still holds: the machine config this branch renders is byte-identical to what current upstream/main renders for the same values on default values (92 lines), on a GPU node group (102) and with registryMirrors set (97). As you expected, the escapes are no-ops on ordinary values and the mirrors block is gated on a non-empty map.

Both "Upgrade impact" gaps are filled. The section now says the one roll is not the only one: an instancetype edit rotates the name later for a group sized by instanceType, which the built-in md0 is, and registryMirrors is a second trigger that did not exist before the merge, with the note that the KubevirtMachineTemplate hash has neither. And the scarce-resource rollout evidence moved out of my comment into the section, all three parts, with the surge and nodeStartupTimeout caveats also added to the release note.

Suites after the merge: 209 in packages/apps/kubernetes, 15 in packages/apps/kubernetes-nodes with your registry_mirrors_test.yaml and serial_console_log_test.yaml included, GOLDEN PARITY clean, make generate no drift, api/apps/v1alpha1 builds with every type covered by a deepcopy.

Pruning and the two hand-maintained literals in talos_config_template_name_test.yaml:37,56 are untouched, still follow-ups as agreed. Merge order noted: #3571 is merged with main and out of draft, so it can go first.

@lexfrei

Copy link
Copy Markdown
Contributor

NOT LGTM. The #3513 port is done and I verified it properly this time, so that blocker is closed. What is left is that the new invariant this PR creates, two templates computing the same name, is guarded in the parent chart and not guarded in kubernetes-nodes, and a mutation walks straight through the suite.

Blocker: the hash input is assembled twice in kubernetes-nodes

The description says rendering the spec in one place makes a name divergence structurally impossible. That holds for packages/apps/kubernetes, where cluster.yaml:723 and talos-reconcile-job.yaml:385 both pass the same $group straight out of the kubernetes.nodeGroups helper. It does not hold for kubernetes-nodes. There nodegroup.yaml:34 builds a ten key $group and talos-reconcile-job.yaml:273 builds a separate four key one, and both feed kubernetes.talosConfigTemplateHash. The rendering is shared, the input assembly is not, so the two sites agree by coincidence of the four keys the helper happens to read.

I dropped "gpus" .Values.gpus from the Job's dict, one line, and nothing caught it. helm unittest . in packages/apps/kubernetes-nodes stayed at 15 of 15 passing and bash tests/render-parity.sh stayed green, which is expected once you read its header: it compares KubevirtMachineTemplate, MachineDeployment, MachineHealthCheck and WorkloadMonitor, and says so at line 19, never the Job. Rendering kubernetes-nodes-myk8s-md0 with gpus: [{name: nvidia.com/GH200}] on the mutated tree gives TCT_NAME=kubernetes-myk8s-md0-ffdbe1 on the Job and configRef.name=kubernetes-myk8s-md0-0bbd2a on the MachineDeployment. On the clean tree both are 0bbd2a. That divergence is the templates do not exist deadlock this PR exists to make impossible, and no worker is ever created.

The suite is not broken, it just does not cover this. Reverting nodegroup.yaml's configRef to the old fixed name does turn nodegroup_test.yaml red, and perturbing the printf format in the Job's $tctName does turn talos_reconcile_job_test.yaml red, so both suites bite. The gap is the fixture shape: the only pair of literals in kubernetes-nodes is rendered with instanceType: "", gpus: [] and kubelet: {}, and every one of those is falsy, so a divergence in how the Job assembles them is invisible. The parent chart does have the GPU shaped pair, and it works: I injected the same drop into cluster.yaml's hash input and talos_config_template_name_test.yaml went red on a machine-config change rotates the name the MachineDeployment references. So the coverage sits on the chart that cannot drift and is missing from the chart that can.

Two ways to close it and I would take the first. Derive the Job's $group from the same expression nodegroup.yaml uses, which removes the seam instead of watching it. Failing that, add a GPU shaped pair to talos_reconcile_job_test.yaml and nodegroup_test.yaml, which at least makes the next divergence fail.

While you are in that file, talos-reconcile-job.yaml:217 still says the computed locals are read as .group, .groupName, and this PR removed the set $jobContext "group" line. Nothing reads .group any more.

#3571

Merging #3571 into this head conflicts in four files, so it will not slip through unnoticed, but the obvious resolution is wrong in a way worth writing down before either of you does it. Keeping this branch's side of talos-reconcile-job.yaml, which is what you would reach for since this branch deleted that region, drops #3571's machine.kernel.modules out of the machine config entirely. I built that tree: with kernelModules: [nvidia, nvidia_uvm] the #3571 head renders kernel: modules: into the config, the merged tree renders no kernel block at all while the config itself renders fine. The per pool schematic goes half way as well: with schematicID set the merged tree emits the pool schematic on the boot disk from nodegroup.yaml, which auto merged, and the global default on the installer inside the machine config, which came from _talosconfigtemplate.tpl. That is precisely the mismatch #3571's own comment warns about, an in-place upgrade swapping the node's extension set out from under it.

The saving grace is that it is loud: helm unittest on that merged tree is 12 failed of 32, so anyone who runs the suite after resolving finds it. Same shape as the #3513 port, port the fields into _talosconfigtemplate.tpl rather than pick a side. This does not change the merge order, #3571 first or both together still looks right.

What I checked and did not find

The #3513 port is complete. I dedented main's inline machine-config block and diffed it against the helper body after normalising the variable renames the move forces, .talosVersion to $talosVersion and $.Values to $root.Values, plus the nindent depth. Four lines differ and all four are the two Helm comments, reworded to describe the new location. Every escaped interpolation is carried over: talosVersion in the quoted field, installerRepository, schematicID, talosVersion in the image tag, and toYaml . for the mirrors. The INVARIANT comment now names both charts, which is the right call given the file is copied.

Execution level agrees. hack/cozytest.sh hack/talos-reconcile-heredoc_test.bats exits 0 with five cases OK, and the hostile mirror endpoint comes back out of a real shell literally, in both charts.

The byte-identity claim holds against the main that exists now. I rendered the Job on origin/main and on this head with tests/values/common.yaml, pulled the heredoc out, parsed it as YAML and hashed the machine config object: identical, 469f3f1d. Positive control on the same instrument, the GPU render differs at 2a85738e and the diff is the nodeLabels block, so it does discriminate.

The README correction is accurate. Setting talos.registryMirrors rotates the name from 35293f to d53117, so the group does roll.

Reservations are in the hash: resources.memory from 4Gi to 16Gi rotates 35293f to a235e1. That is the mechanism behind the instancetype roll trigger you now describe in the upgrade note.

Nothing dangles after the move. Every variable the diff removed from $jobContext, kubeletVersion, talosVersion, podCIDR, serviceCIDR, mgmtClusterDomain and the six reservation locals, has zero remaining uses in either Job template; the one .group hit is the stale comment above. No consumer of the old fixed template name anywhere else in the tree either, the Go references are comments and the <cluster>-<group> literals in nodegroup.yaml and dashboard-resourcemap.yaml are MachineDeployment, MachineHealthCheck and WorkloadMonitor names.

The kubernetes-nodes Makefile change is safe: update is declared phony at line 3, so the added cp runs, and there is no file in the package directory that shadows a target.

A failed apply is loud rather than silent, which also covers the truncated hash. The container runs sh -ec and kubectl apply is last in its pipeline, so a webhook rejection aborts the Job instead of logging applied and exiting 0.

Base state on the branch: 209 tests in packages/apps/kubernetes, 15 in packages/apps/kubernetes-nodes, render-parity.sh reports GOLDEN PARITY.

Still open from earlier rounds

Pruning superseded templates and the hand maintained literal pairs are both unchanged and both were follow-ups. There are four such pairs now across the two charts. The bot's suggestion to add the notMatchRegex guard to kubernetes-nodes/tests/talos_reconcile_job_test.yaml, which the parent suite has, is still worth taking.

Not verified

No cluster, so everything in the upgrade-impact section about CAPI's rollout, the MachineHealthCheck interaction and your GPU fleet numbers is taken as reported. The instancetype roll trigger I confirmed only through the reservation path, not through an actual instancetype edit.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One blocker, in the guard rather than in the render.

In kubernetes-nodes the content hash input is assembled twice: nodegroup.yaml builds its dict from ten keys, talos-reconcile-job.yaml builds a separate one from four. Both feed kubernetes.talosConfigTemplateHash. The render is shared, the input assembly is not. In the parent chart this cannot happen, since both sites take the same $group from kubernetes.nodeGroups, so the claim that name divergence is structurally impossible holds for one of the two charts.

Removing "gpus" .Values.gpus from the Job dict, one line, leaves the suite green at 15/15 and render-parity.sh green, while a group with a GPU renders TCT_NAME=kubernetes-myk8s-md0-ffdbe1 against configRef.name=kubernetes-myk8s-md0-0bbd2a. That is the exact templates do not exist deadlock this PR exists to prevent. The cause is fixture shape: the only literal pair in kubernetes-nodes renders with every relevant value falsy, so the divergence has nothing to bite on. The same mutation in the parent chart does go red, because that one has a GPU pair.

Today the two dicts agree and the rendered output is correct, so this is a hole in the guard rather than a live defect. I am still marking it blocking because the PR introduces the invariant and states its structural guarantee, and in half the cases nothing enforces it.

Details, including the interaction with #3571, are in the comment above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/kubernetes Issues or PRs related to the tenant Kubernetes app kind/bug Categorizes issue or PR as related to a bug size/XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TalosConfigTemplate is immutable but applied on every reconcile, so machine-config changes never propagate

2 participants