Skip to content

Platform-wide defaults for tenant Talos worker settings (imageFactoryURL + registryMirrors) #3950

Description

@IvanHunters

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-values secret (fixed template, arbitrary keys rejected),
  • not the Platform Package CR (spec.components.*.values targets 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):

  1. operator-facing platform values: default talos.imageFactoryURL and talos.registryMirrors (and optionally installerRepository),
  2. rendered into _cluster in packages/core/platform/templates/apps.yaml,
  3. 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 imageFactoryURL does 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

  1. added
    kind/featureCategorizes issue or PR as related to a new feature
    area/kubernetesIssues or PRs related to the tenant Kubernetes app
    on Aug 24, 2026
  2. changed the title [-]Platform-wide default for tenant Kubernetes worker image source (imageFactoryURL / registryMirrors)[/-] [+]Platform-wide defaults for tenant Talos worker settings (imageFactoryURL + registryMirrors)[/+] on Aug 24, 2026
  3. lexfrei commented on Sep 1, 2026

    @lexfrei
    Contributor

    Worth fixing the precedence chain before anyone implements this, because the failure mode is silent.

    per-CR > _cluster > built-in default cannot be written as a plain default chain. packages/apps/kubernetes-nodes/values.yaml has imageFactoryURL: "https://factory.talos.dev" on line 126 and installerRepository: "factory.talos.dev/installer" on line 127, both non-empty chart defaults. Something like .Values.talos.imageFactoryURL | default $cluster.imageFactoryURL then 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. registryMirrors on 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.yaml and 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 parent kubernetes chart. All three knobs are read there, nodegroup.yaml:89 for the factory URL. The parent chart reads only .Values.talos.version, at cluster.yaml:21-25. The file citations in the issue point at the parent and predate the split.

  4. SerjioTT commented on Sep 7, 2026

    @SerjioTT
    Contributor

    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 KubernetesNodes still goes to factory.talos.dev unless whoever created it set the field by hand.

    A third channel has appeared since this was filed. #3294 adds osImage on KubernetesNodes, where osImage.builtin clones a golden from an opt-in catalog. The catalog side got a platform knob in that PR — kubernetesWorkerImage forwarded into the Package component values — so an operator can point the single import at a mirror through platform configuration. The consuming side did not: osImage defaults to {} and nodegroup.yaml reads no _cluster key 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. imageFactoryURL is not only a value the render reads: packages/apps/kubernetes-nodes/templates/nodegroup.yaml:89 puts it into the DataVolume URL inside the kubevirtmachinetemplate define, and line 477 hashes that whole define into the KubevirtMachineTemplate name. A _cluster fallback 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.

    registryMirrors does 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 hooks normalizeSpecNulls into Create and Update in rest.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 default in the values schema. Defaults are applied on read in cozystack-api (applySpecDefaults, called from ConvertHelmReleaseToApplication) rather than written into the HelmRelease, so a schema default surfaces on every existing pool at once and gets persisted by the first kubectl apply or dashboard edit that follows — the same fleet roll, only delayed and unpredictable in timing.

  5. myasnikovdaniil commented on Sep 8, 2026

    @myasnikovdaniil
    Contributor

    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.

    registryMirrors and installerRepository are read only in kubernetes-nodes/templates/talos-reconcile-job.yaml (:338 and :332), and that Job applies a TalosConfigTemplate with a stable name, kubernetes-<cluster>-<group>, which is what MachineDeployment.bootstrap.configRef points 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.

    imageFactoryURL is the different one. It renders into url at nodegroup.yaml:89, inside the block hashed at :477, and MachineDeployment.infrastructureRef uses 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 default chain 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 consumer containerd-registry-secret.yaml is 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 kubernetes chart still exposes all three fields in values.yaml:255-259, the schema, the README and the dashboard form, while no template there reads them after the split, only talos.version (cluster.yaml:21). An operator sets a mirror on the Kubernetes CR and gets silence, no error anywhere. Probably its own issue.

    For imageFactoryURL the only shape that gives a platform default without rolling is stamping the value into the spec at create, as Sergei Makarov (@SerjioTT) put it on #3294. Defaults are applied on read only today, the write path stores the spec verbatim, and #3956 already hooks Create and Update in rest.go.

    registries.config (overridePath, auth, #3331) I have not decided: either map it into the Talos shape or carry endpoints only on the first step.

  6. added
    triage/acceptedIndicates an issue is ready to be actively worked on
    priority/important-longtermImportant over the long term, but may not be staffed and/or may need multiple releases to complete
    and removed
    triage/needs-triageIndicates an issue needs triage by a maintainer
    on Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area/kubernetesIssues or PRs related to the tenant Kubernetes appkind/featureCategorizes issue or PR as related to a new featurepriority/important-longtermImportant over the long term, but may not be staffed and/or may need multiple releases to completetriage/acceptedIndicates an issue is ready to be actively worked on

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions