Skip to content

feat(provider): add the cozystack_kubernetes_nodes resource - #24

Merged
Aleksei Sviridkin (lexfrei) merged 7 commits into
feat/cozystack-1.6from
feat/cozystack-1.6-kubernetesnodes
Aug 13, 2026
Merged

Aleksei Sviridkin (lexfrei) merged 7 commits into
feat/cozystack-1.6from
feat/cozystack-1.6-kubernetesnodes

Conversation

@lexfrei

Copy link
Copy Markdown
Collaborator

Pull Request

Summary

Cozystack 1.6 introduced the KubernetesNodes kind, which splits worker node pools out of the Kubernetes CR so pools can be managed independently of the parent cluster. This adds the cozystack_kubernetes_nodes resource and matching data source, the only upstream app kind the provider did not register.

Defaults follow two rules. Stable semantic defaults (storage class, replica bounds, instance type, health checks, version) are materialised, mirroring the kubernetes kind. Drift-prone fields (talos, images, kubelet, roles, gpus) stay presence-preserving: the aggregated apiserver resolves their defaults on every read, and a Computed attribute would write the server-resolved Talos values back as explicit spec keys, pinning the pool to the Talos release current on creation day. Import is the deliberate exception so existing overrides are captured instead of silently deleted on the next update.

The CR must be named <cluster>-<pool>; a name colliding with a parent-managed node group fails on ownership conflict. Documented on the resource page.

Changes

  • Add the KubernetesNodes client resource descriptor and register resource plus data source
  • Typed model with spec-coverage guards for the nested talos, kubelet, images and GPU surfaces
  • Add a config-validation hook to the generic resource machinery (no-op for every other kind)

Testing

  • Unit tests pass locally (make test): 16 new KubernetesNodes tests pass; the branch inherits 7 spec-coverage failures from the integration base (v1.6.1 pin bump), fixed by the sibling PRs. Zero new failures, verified against the merge base.
  • Linters pass locally (make lint)
  • Generated docs are up to date (make docs produces no diff)
  • Acceptance tests pass against a live cluster (make testacc), if applicable: needs a live parent cluster, planned as a follow-up before release

Documentation

  • Examples added or updated under examples/
  • Registry docs regenerated (make docs)
  • CHANGELOG updated (if user-facing): a consolidated v1.6 section lands with the integration PR

Checklist

  • Commit messages follow semantic format (type(scope): description)
  • No secrets or credentials in code or state
  • Breaking changes documented (none in this PR)

Additional Notes

Targets the feat/cozystack-1.6 integration branch, not master.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a8e9c48e-ffe9-4568-8e9b-b265b610dfc1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict

NOT LGTM

The resource itself is well-designed and its core claim holds up: there is no provider-side default that would pin a pool to the Talos release of its creation day, the presence-preserving round-trip is sound, and the new KubernetesNodes tests all pass. The change is not approved only because of one concrete, author-actionable blocker plus two minor items below. Fix the sign-off and this is essentially an approve.

Blocking

  • [MAJOR] DCO check is red: a commit is not signed off. Commit 13100ebd8ead ("feat(provider): model the KubernetesNodes worker pool spec") lacks a Signed-off-by trailer; the other three commits have it. The PR cannot merge while DCO fails. Please git rebase --signoff feat/cozystack-1.6 (or amend that one commit) and force-push.

CI analysis (the red test check is inherited, not introduced here)

The test job fails only on *ExpandKeysMatchConfigSpec coverage guards for kinds this PR does not modify: tenant.gateway, cozystack_kubernetes oidc/talos/nodeHealthCheck/maxUnhealthy/nodeStartupTimeout, and nats/qdrant tls. This PR adds the cozystack_kubernetes_nodes resource and touches none of those models. The same tenant/kubernetes failures appear on sibling branch #22, which also does not touch them, confirming they live in the stacked base feat/cozystack-1.6 and are resolved by other PRs in the stack (#22 for tls, #25 for the kubernetes/tenant surface). The resource's own guards (TestKubernetesNodesExpandNestedKeysMatchTheirSpecs, naming/validation, three-state) all pass. So CI-red is not a reason for the NOT LGTM; the DCO sign-off is.

What was verified

  • No creation-day Talos pin. expand emits talos/images/kubelet (at minimum {}) and omits roles/gpus when null; unset fields stay server-managed rather than being frozen to a default, so a pool is not silently pinned. keepConfigured* maps server-echoed empties back to null, so an unset block does not produce perpetual drift.
  • Whole-spec replacement on update is real. client.Update does current.Object["spec"] = app.Spec (application.go), so any field present on the object but absent from config is dropped on the next apply. This is the correct, visible behaviour (capture-on-import surfaces the removal as a plan diff rather than losing it silently), but see the docs note below.
  • version enum and naming validation match the nodes chart values.yaml; go vet and the new tests pass.

Minor findings

  • [MINOR] kubernetes_nodes_schema.go:132: the pool version enum is OneOf("v1.35".."v1.31") and omits v1.30, which the parent cozystack_kubernetes version (kubernetes_schema.go:69) still accepts. Both the docs and the shipped example wire version = cozystack_kubernetes.cluster.version, so an operator running a parent cluster on v1.30 who copies the example gets a plan-time validation error on the pool. The enum matches the nodes chart, so this is a cross-resource inconsistency rather than a nodes-side bug, but worth reconciling or documenting.
  • [MINOR] Import docs (docs/resources/kubernetes_nodes.md, Import section): because expand replaces the whole spec, any override captured on import (for example an air-gapped talos.installer_repository) that is not copied into the .tf config is dropped on the first apply. The capture-on-import design is the right choice (it makes the loss a visible plan diff instead of silent), but the docs should tell operators to reconcile talos/images/kubelet/roles/gpus into config before applying.

Cozystack 1.6 splits worker node pools out of the Kubernetes CR into a
standalone KubernetesNodes kind, served by the aggregated API under the
plural kubernetesnodeses. Declaring the descriptor lets the provider
address the kind through the same dynamic-client CRUD every other app
kind uses.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
A pool carries the sizing fields a Kubernetes nodeGroup already carried
plus kubelet reservations, a Talos image block, and image overrides.

The aggregated API resolves the chart's schema defaults into the spec on
every read, so a block the pool never configured comes back populated.
Taking those values into state would write them back as explicit spec
keys on the next update, freezing the pool on the Talos release and
images that were current the day it was created — while the parent
cluster, which does not model them at all, keeps following the chart.
The talos, kubelet, and images blocks are therefore optional and not
computed, and a field the configuration left unset stays out of state.
The values that are semantically stable keep an explicit default,
matching how the Kubernetes kind models the same fields.

The read after an import is the exception and records everything the
server reports. It is the only chance to capture what a pool already
has, and hiding an existing override there would let the next update,
which replaces the spec whole, delete it with nothing in the plan.

The chart derives the pool name by stripping the parent cluster from the
release name and fails the render when that prefix is missing, which
otherwise lands as a HelmRelease that can never install. Name and
cluster both force replacement, so a mistyped rename plans as destroy
plus create and a check that waited for Create would fire with the pool
already torn down; the generic resource therefore grows an optional
hook that lets a kind reject a configuration while the plan is built.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
KubernetesNodes was the one upstream app kind the provider did not
serve. Registering the resource and its data source alongside the
Kubernetes kind makes worker pools manageable on their own, which is the
point of the split.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The example pairs a cluster with a standalone GPU pool so the naming
rule is visible in the code rather than only in prose: the pool object
is <cluster>-<pool>, and the pool part cannot reuse a group the parent
cluster still manages.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
An update replaces the whole spec, so an override captured on import
and left out of configuration is removed by the first apply. The plan
shows the removal, but only the configuration can prevent it.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the feat/cozystack-1.6-kubernetesnodes branch from 0564564 to 64339f1 Compare August 12, 2026 23:54
@lexfrei

Copy link
Copy Markdown
Collaborator Author

Fixed: rebased with sign-off, every commit now carries a Signed-off-by matching the author. Also took the import note: the resource description now tells importers to copy captured overrides into configuration before the first apply (docs regenerated). On the v1.30 mismatch: #25 drops v1.30 from the parent validator, so the two resources agree once the stack lands.

…ass changes

The aggregated apiserver accepts a storage class change without migrating
any volume, so an in-place update records a class the disks do not live
on. Matches the behaviour every other kind adopted in this release.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
@lexfrei
Aleksei Sviridkin (lexfrei) merged commit d000174 into feat/cozystack-1.6 Aug 13, 2026
5 of 6 checks passed
Aleksei Sviridkin (lexfrei) added a commit that referenced this pull request Aug 13, 2026
* feat(client): add the KubernetesNodes resource descriptor

Cozystack 1.6 splits worker node pools out of the Kubernetes CR into a
standalone KubernetesNodes kind, served by the aggregated API under the
plural kubernetesnodeses. Declaring the descriptor lets the provider
address the kind through the same dynamic-client CRUD every other app
kind uses.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>

* feat(provider): model the KubernetesNodes worker pool spec

A pool carries the sizing fields a Kubernetes nodeGroup already carried
plus kubelet reservations, a Talos image block, and image overrides.

The aggregated API resolves the chart's schema defaults into the spec on
every read, so a block the pool never configured comes back populated.
Taking those values into state would write them back as explicit spec
keys on the next update, freezing the pool on the Talos release and
images that were current the day it was created — while the parent
cluster, which does not model them at all, keeps following the chart.
The talos, kubelet, and images blocks are therefore optional and not
computed, and a field the configuration left unset stays out of state.
The values that are semantically stable keep an explicit default,
matching how the Kubernetes kind models the same fields.

The read after an import is the exception and records everything the
server reports. It is the only chance to capture what a pool already
has, and hiding an existing override there would let the next update,
which replaces the spec whole, delete it with nothing in the plan.

The chart derives the pool name by stripping the parent cluster from the
release name and fails the render when that prefix is missing, which
otherwise lands as a HelmRelease that can never install. Name and
cluster both force replacement, so a mistyped rename plans as destroy
plus create and a check that waited for Create would fire with the pool
already torn down; the generic resource therefore grows an optional
hook that lets a kind reject a configuration while the plan is built.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>

* feat(provider): register cozystack_kubernetes_nodes

KubernetesNodes was the one upstream app kind the provider did not
serve. Registering the resource and its data source alongside the
Kubernetes kind makes worker pools manageable on their own, which is the
point of the split.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>

* docs(kubernetes-nodes): add pool examples and generated pages

The example pairs a cluster with a standalone GPU pool so the naming
rule is visible in the code rather than only in prose: the pool object
is <cluster>-<pool>, and the pool part cannot reuse a group the parent
cluster still manages.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>

* docs(kubernetes-nodes): tell importers to reconcile captured overrides

An update replaces the whole spec, so an override captured on import
and left out of configuration is removed by the first apply. The plan
shows the removal, but only the configuration can prevent it.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>

* feat(kubernetes-nodes): replace the pool when a configured storage class changes

The aggregated apiserver accepts a storage class change without migrating
any volume, so an in-place update records a class the disks do not live
on. Matches the behaviour every other kind adopted in this release.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>

---------

Signed-off-by: Aleksei Sviridkin <f@lex.la>
@lexfrei
Aleksei Sviridkin (lexfrei) deleted the feat/cozystack-1.6-kubernetesnodes branch August 13, 2026 00:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants