Repository navigation
Description
Problem / use case
Operators running Cozystack in closed, rate-limited or air-gapped environments need every new tenant Kubernetes cluster to source its worker-node settings from a cluster-local default, without setting talos.* on every Kubernetes CR by hand. This concerns two separate channels of a tenant Talos worker:
- Worker OS disk image — fetched by CDI as a raw HTTP artifact (
{imageFactoryURL}/image/.../openstack-amd64.raw.xz,packages/apps/kubernetes/templates/cluster.yaml), outside any registry. Knob:talos.imageFactoryURL. - In-guest container-image pulls (installer, kubelet, workload images) — routed via
machine.registries.mirrors. Knob:talos.registryMirrors.
Today both are per-CR only. There is no operator-facing layer for a cluster-wide default:
- not platform values, not ApplicationDefinition (no
defaultValues), - not the
cozystack-valuessecret (fixed template, arbitrary keys rejected), - not the Platform Package CR (
spec.components.*.valuestargets platform components, not tenant apps).
For registryMirrors this is a regression introduced by the kubeadm→Talos rollover: under kubeadm the platform-wide registries.mirrors value (packages/core/platform/values.yaml) reached tenant workers via a Helm-rendered *-patch-containerd Secret (containerd certs.d). That Secret has no consumer in the Talos machineconfig and was removed; the platform-wide registries value no longer reaches tenant workers at all (packages/apps/kubernetes/README.md:90). So a closed-environment operator who could previously set registry mirrors once, platform-wide, must now repeat talos.registryMirrors on every tenant CR.
Result: in a closed environment an operator injects mirror/source config into every tenant CR at their own provisioning layer, and any tenant created without it falls back to public factory.talos.dev / upstream registries — exactly the egress these environments block.
Proposed direction
Carry platform defaults for tenant Talos worker settings through the existing _cluster channel — the same mechanism the kubernetes chart already reads for _cluster.scheduling (cluster.yaml):
- operator-facing platform values: default
talos.imageFactoryURLandtalos.registryMirrors(and optionallyinstallerRepository), - rendered into
_clusterinpackages/core/platform/templates/apps.yaml, - consumed as a fallback in the kubernetes chart, precedence
per-CR > _cluster > built-in default (public factory / upstream).
Per-CR override stays intact; no new API surface — reuses the _cluster passthrough that already exists. For registryMirrors this restores, on Talos rails, the platform-wide reach the kubeadm patch-containerd path used to provide.
Notes
- The two channels are distinct: a registry mirror does not cover the OS disk image (raw HTTP via CDI, outside any registry), and
imageFactoryURLdoes not cover in-guest container pulls. A complete solution needs both. - Related: feat(kubernetes): boot tenant worker disks from a shared golden Talos image (CDI clone) #3294 (golden worker images / CDI clone) reduces repeat disk pulls but adds no platform default source.
- Historical context for the registryMirrors regression:
packages/apps/kubernetes/README.md:90. - Requested by an operator building a public cloud on Cozystack in a firewall-restricted environment.
Activity
[-]Platform-wide default for tenant Kubernetes worker image source (imageFactoryURL / registryMirrors)[/-][+]Platform-wide defaults for tenant Talos worker settings (imageFactoryURL + registryMirrors)[/+]lexfrei commentedon Sep 1, 2026
Worth fixing the precedence chain before anyone implements this, because the failure mode is silent.
per-CR > _cluster > built-in defaultcannot be written as a plain default chain.packages/apps/kubernetes-nodes/values.yamlhasimageFactoryURL: "https://factory.talos.dev"on line 126 andinstallerRepository: "factory.talos.dev/installer"on line 127, both non-empty chart defaults. Something like.Values.talos.imageFactoryURL | default $cluster.imageFactoryURLthen resolves to the chart default in every render, because Helm cannot tell "the operator did not set this" from "the operator set it to the same string as the default". The platform default never wins, the render is clean, and nothing reports an error anywhere.registryMirrorson line 128 is{}, so that knob is the one of the three that needs no special handling.Two ways out. Empty those two keys in
values.yamland move the public factory literal into the template as the last fallback, or use an explicit sentinel. Either works.Second thing, cheap and in the same area: the consumer belongs in
kubernetes-nodes, not the parentkuberneteschart. All three knobs are read there,nodegroup.yaml:89for the factory URL. The parent chart reads only.Values.talos.version, atcluster.yaml:21-25. The file citations in the issue point at the parent and predate the split.SerjioTT commentedon Sep 7, 2026
Operator case for the record, since the issue says it came from one and we are in the same position: our clusters cannot reach the public Image Factory at all. The mirror is up and a golden image is imported, and a newly created
KubernetesNodesstill goes tofactory.talos.devunless whoever created it set the field by hand.A third channel has appeared since this was filed. #3294 adds
osImageonKubernetesNodes, whereosImage.builtinclones a golden from an opt-in catalog. The catalog side got a platform knob in that PR —kubernetesWorkerImageforwarded into the Package component values — so an operator can point the single import at a mirror through platform configuration. The consuming side did not:osImagedefaults to{}andnodegroup.yamlreads no_clusterkey for the image source. The list of channels here is now three, and the third has the same shape as the first two.The proposed direction has a consequence worth settling before anyone implements it, and it is larger than the precedence problem Aleksei Sviridkin (@lexfrei) raised above.
imageFactoryURLis not only a value the render reads:packages/apps/kubernetes-nodes/templates/nodegroup.yaml:89puts it into the DataVolume URL inside thekubevirtmachinetemplatedefine, and line 477 hashes that whole define into the KubevirtMachineTemplate name. A_clusterfallback consumed at render time therefore changes the resolved URL of every pool that relied on the built-in default, the moment an operator first sets the platform value, which re-hashes the KMT and rolls every worker in the fleet. myasnikovdaniil reached the same conclusion independently in the #3294 thread, and noted that fixing the precedence trap is what makes the hazard reachable rather than what avoids it.That is the exact invariant #3294 is built around, and the one its approving review verified by rendering both sides byte-identical. The two efforts are in tension on this point today.
registryMirrorsdoes not have this property, since it reaches the guest through the machine config rather than through the disk template. So the two channels need different answers even though the issue treats them as one shape.The alternative that keeps the no-roll invariant by construction is to apply the default when a resource is created rather than when it is rendered: a platform-level setting stamped into the spec of a new
KubernetesNodes, never applied to one that already exists. Stored values of existing releases stay untouched, so nothing re-renders and nothing rolls, while a new pool arrives with the operator's choice already in its spec, as an ordinary field from then on. #3956 is opening the seam for it: it hooksnormalizeSpecNullsintoCreateandUpdateinrest.go, so a create-only defaulting step has somewhere to sit.The tradeoff should be stated rather than discovered, because #3956's own description names it: writing a value into the spec pins the application to it, so a later change to the default never reaches a pool that already has one. For a fleet default that is the intended behaviour rather than a defect — an operator who repoints a mirror wants new pools to follow and existing ones to stay put until they are deliberately re-imaged, which is the same bargain the per-CR field already offers. It does mean the platform value is a default for new resources and not a fleet-wide setting, and the docs should say so in those words.
One shape that looks like this and is not: a
defaultin the values schema. Defaults are applied on read incozystack-api(applySpecDefaults, called fromConvertHelmReleaseToApplication) rather than written into the HelmRelease, so a schema default surfaces on every existing pool at once and gets persisted by the firstkubectl applyor dashboard edit that follows — the same fleet roll, only delayed and unpredictable in timing.feat(kubernetes): boot tenant worker disks from a shared golden Talos…
myasnikovdaniil commentedon Sep 8, 2026
Three knobs in this issue are not one class, and the proposed direction is safe for two of them and a fleet roll for the third.
registryMirrorsandinstallerRepositoryare read only inkubernetes-nodes/templates/talos-reconcile-job.yaml(:338 and :332), and that Job applies a TalosConfigTemplate with a stable name,kubernetes-<cluster>-<group>, which is whatMachineDeployment.bootstrap.configRefpoints at. An unchanged render re-applies a byte-identical TCT, a CAPI no-op, nothing rolls, and the comment at :33-35 says exactly that. So a platform default is safe for these two. It lands in a worker at bootstrap though, so already running nodes keep the config they were created with until they are replaced.imageFactoryURLis the different one. It renders intourlatnodegroup.yaml:89, inside the block hashed at:477, andMachineDeployment.infrastructureRefuses that hashed name (:80). The day an operator sets the platform default, every pool that never set the field per-CR gets a new KubevirtMachineTemplate name and rolls its workers.This chains with Aleksei Sviridkin (@lexfrei)'s comment above: fixing the precedence trap is what activates the roll. Today a plain
defaultchain would silently never win, so nothing happens at all, and the moment precedence works the first render moves the hash. Order matters here, not only correctness.For mirrors this is not a new operator knob. The platform already has
registries.mirrors(packages/core/platform/values.yaml:573-600) in the same host to{ endpoints: [ ... ] }shape, and its only consumercontainerd-registry-secret.yamlis dead, which is #4006. Restoring reach of that value and deleting the dead renderer are one change: doing either alone leaves the operator setting lost or keeps rendering a Secret nobody reads.Separate from the missing default, and worse in a way: the parent
kuberneteschart still exposes all three fields invalues.yaml:255-259, the schema, the README and the dashboard form, while no template there reads them after the split, onlytalos.version(cluster.yaml:21). An operator sets a mirror on theKubernetesCR and gets silence, no error anywhere. Probably its own issue.For
imageFactoryURLthe only shape that gives a platform default without rolling is stamping the value into the spec at create, as SerjioTT put it on #3294. Defaults are applied on read only today, the write path stores the spec verbatim, and #3956 already hooksCreateandUpdateinrest.go.registries.config(overridePath, auth, #3331) I have not decided: either map it into the Talos shape or carryendpointsonly on the first step.feat(kubernetes): boot tenant worker disks from a shared golden Talos…