Skip to content

feat(provider): model the Cozystack 1.6 kubernetes and tenant surface - #25

Open
Aleksei Sviridkin (lexfrei) wants to merge 24 commits into
feat/cozystack-1.6from
feat/cozystack-1.6-core
Open

feat(provider): model the Cozystack 1.6 kubernetes and tenant surface#25
Aleksei Sviridkin (lexfrei) wants to merge 24 commits into
feat/cozystack-1.6from
feat/cozystack-1.6-core

Conversation

@lexfrei

Copy link
Copy Markdown
Collaborator

Pull Request

Summary

Models the Cozystack 1.6 surface on the kubernetes and tenant kinds: new talos, node_health_check, oidc, control_plane.api_server and images blocks, per-node-group health overrides, the tenant.gateway flag, and the v1.30 removal from the version validator. All three spec-coverage guards that went red after the v1.6.1 pin bump are green again through modelling; no omit list grew.

Defaults design: no provider-side default on any new 1.6 field, and the resource stores only what the configuration sets. The aggregated apiserver materialises chart defaults on every read, so a resource that stored them would write them back as explicit spec keys on the next update and silently pin the cluster to the Talos release and schematic of creation day. The data source still reports the full effective surface.

Breaking changes: version = "v1.30" now fails at plan time; a configured storage_class change now replaces the cluster (the server accepts the write but the PVC never migrates); storage_class lost its provider-side replicated default for the same reason.

Changes

  • Model talos, node_health_check, oidc, control_plane.api_server and images on the kubernetes resource and data source, each with a nested drift guard
  • Add per-node-group max_unhealthy and node_startup_timeout, bind resources.cpu/memory both-or-neither at plan time, fix node_groups = {} and roles = [] failing apply
  • Pin the tenant.gateway three-state contract with tests and corrected descriptions
  • storage_class: RequiresReplaceIfConfigured, provider default removed, v1.30 dropped from the version validator

Testing

  • Unit tests pass locally (make test): green except the 4 tls spec-coverage guards inherited from the integration base, owned by the sibling tls PR. Identical at the merge base, zero new failures.
  • 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: new acc steps added for the update-does-not-pin case and the empty-collection cases, they need a live 1.6 cluster

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 (see Summary)

Additional Notes

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

Cozystack 1.6 moved tenant worker nodes from Ubuntu container disks to
Talos, and the image coordinates became part of the Kubernetes spec. Left
unmodelled, an operator has no way to point workers at a self-hosted image
factory or a custom schematic without dropping out of Terraform.

None of the four fields carries a provider-side default. Upstream moves the
Talos release and the tested schematic ID with every platform release, so a
materialised default would pin a cluster to whatever was current when the
provider was built and silently detach it from the platform's rolling
value. Unset fields are omitted from the emitted spec instead; the
aggregated apiserver materialises its own defaults on every read, so state
still reports the effective value.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The 1.6 chart remediates worker nodes through a MachineHealthCheck whose
tolerance and startup timeout are now part of the Kubernetes spec. Slow
first boots — a Talos image pulled from the image factory onto a busy
StorageClass — otherwise trip remediation and put the node group into a
reboot loop with no way to raise the timeout from Terraform.

Both fields are omitted from the spec while unset, so the platform's own
tuning stays in force and moves with it.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Without this block the only way into a tenant cluster is the static admin
kubeconfig, which cannot be scoped per person and cannot be revoked
individually. Modelling it lets an operator turn on platform identity and
declare the per-user bindings in the same place the cluster is declared.

mode and the user roles are validated against the upstream enums, so a typo
fails at plan time rather than as a chart render error. custom_config takes
either an inline AuthenticationConfiguration or a Secret reference and the
two conflict, matching the chart, which reads only one. users keeps its
presence distinction: an explicitly empty list binds nobody, an unset
attribute leaves the platform default in place.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
A node group that boots slower than the rest — a GPU pool pulling a large
Talos image, say — needs a longer startup timeout than the cluster-wide
one, and 1.6 added the per-group knobs for exactly that. Both are
undefaulted upstream, where an absent key means "inherit the cluster-wide
value", so they are written only when set rather than as empty strings.

The node group's resources block now binds cpu and memory together.
Upstream sizes the node by instanceType unless both are set, because
KubeVirt cannot override an instance type's CPU and memory, and rejects a
half-filled block at render time; the schema turns that into a plan-time
error instead of a failed apply. The shared resources helper keeps its own
any-field-wins semantics for the kinds that want it.

The new model round-trip test binds the whole fixture through the resource
schema, which is what catches a tfsdk tag that names no attribute — a
failure mode the expand and flatten tests never reach.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Cozystack 1.6 removed v1.30 from the Kubernetes version enum. The provider
still advertised it, so a config naming v1.30 planned cleanly and then
failed against the server — the worst shape of validation error, since it
surfaces after the practitioner has already committed to the apply.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
controlPlane was unmanaged on purpose while it held nothing but component
sizing. 1.6 added the apiServer passthrough — extra flags, extra volumes,
extra volume mounts — which is the only supported way to hand the tenant
kube-apiserver a feature gate or an AuthenticationConfiguration file. That
is worth managing; the sizing, replica count, konnectivity and scheduler
blocks stay with the server and are recorded as deliberate omissions in the
new nested coverage guard.

The two volume lists are free-form core/v1 objects upstream, so they travel
as normalized JSON strings rather than a hand-modelled Volume schema that
would need chasing every core/v1 addition. The attribute description
carries the upstream warning about hand-rolled --oidc-* flags, which make
the apiserver refuse to start when the chart also injects
--authentication-config.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
An air-gapped or rate-limited environment needs the tenant-side images
pulled from a mirror, and 1.6 exposes all three of them — the bootstrap
kubectl Job, the talos-csr-signer sidecar, the wait-for-kubeconfig init
container. Until now the whole block was unmanaged, which meant no mirror
without editing the release by hand.

No field carries a provider-side default. Upstream treats an empty value as
"use the tag this chart shipped with", and that tag moves every release, so
writing one from the provider would hold a cluster on an image the platform
has already replaced.

With controlPlane and images now modelled, the top-level coverage guard
omits only addons.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
gateway landed in the pinned API, so the coverage guard now demands it and
the attribute stops being a speculative passthrough. Its encoding is the
subtle part and had no test: the chart distinguishes "unset" from
"explicitly off" by whether the key is present, not by its value. An unset
attribute therefore has to leave the key out — writing `gateway: null`
fails the schema generated from the field's own documentation, and writing
`false` silently takes every derived-apex tenant off the auto-enable path.

The behaviour was already right; this makes it a contract. The table test
covers all four states, a second test covers the read direction, and the
attribute description now explains what the platform decides on the
operator's behalf instead of describing the field as version-gated.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Upstream marks the cluster storageClass immutable with a CEL rule, but the
aggregated apiserver does not evaluate CEL rules embedded in an application
schema, so the write is accepted. That is worse than a rejection: apply
reports success, state records the new class, and every existing volume
stays on the old one, because a PersistentVolumeClaim's class is fixed at
creation and editing a StatefulSet's volumeClaimTemplates never migrates
data. The divergence is permanent and invisible.

Planning a replacement makes the cost of the change visible before it
happens, which is the only place a practitioner can still decide against
it. The per-node-group storageClass is deliberately left mutable, matching
upstream, which does not mark it immutable precisely because a strict rule
would block setting an optional field for the first time.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The examples now show what each new block is for rather than listing every
attribute: a node group sized by explicit cpu/memory, a slow GPU pool
overriding only its own remediation timeout, a self-hosted image factory
with the Talos release still left to the platform, platform identity with
user bindings, a bring-your-own issuer with the apiserver passthrough, and
mirrored images. The tenant example covers the case that motivates the
gateway attribute at all — a custom apex that wants a Gateway anyway,
which the platform would not enable on its own.

Both example files were checked against the real schema with `tofu
validate`, and the resource description no longer claims the control-plane
and image blocks are unmanaged.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The 1.6 blocks deliberately carry no provider-side defaults, which only
works because the aggregated apiserver materialises the schema defaults on
every read. That assumption is invisible to unit tests — they never talk to
a server — so the acceptance test now pins it: a cluster that pins nothing
must still report a Talos release, a health-check tolerance, and an OIDC
mode.

The negative checks matter as much. A node group's health overrides are
undefaulted upstream and must stay absent, and an unset tenant gateway must
stay absent so the platform keeps deciding. Either one turning into an
empty string or a false would be the encoding quietly collapsing.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The package splits its name constants two ways: attr* when the Terraform
attribute and the spec key are the same word, spec* when the camelCase form
differs. Three of the new blocks fell on the attr* side but were named
spec*, which reads as though the schema were keyed by a spec name.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The unset-block contract held only until the first update. Those blocks are
Optional+Computed so the platform's effective values can land in state, and
Terraform copies prior state into the plan wherever the configuration is
silent — so expanding the plan alone sent the platform's own defaults back
as explicit spec keys. From that write on, the release carried
talos.version and the schematic ID, the chart's defaults no longer applied,
and the cluster sat frozen on whatever was current the day something
unrelated changed. Nothing in the plan showed it.

Models can now take the configuration as the authority for the attributes
they nominate, through an optional hook the shared Create/Update path calls
before expand. Only cozystack_kubernetes uses it, and only for the five
nested blocks; every other attribute still expands from the plan, where its
materialised default belongs. The engine refuses to run a model that wants
the configuration through a path that cannot supply it, rather than
silently expanding the plan.

Two smaller cases of the same "emit only what was asked for" rule: an oidc
secretRef with no name, and a controlPlane with no apiServer, no longer
travel as empty objects.

The unit tests pin the round trip that was missing — a plan that only
echoes a server response must still expand to no keys — and the acceptance
test now runs a second apply, the one that used to do the pinning, and
reads the release back to prove the keys stayed out.

The JSON-document list helpers move to spec.go, where the other spec
helpers live; they are not kubernetes-specific.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
An unconditional RequiresReplace reads the plan, and the plan is not only
what the practitioner wrote. Import a cluster that runs on a non-default
class with a configuration that never mentions storage_class, and the
schema default pulls the plan back to "replicated" — which the modifier
then turns into a destroy and recreate of a cluster, from a configuration
that says nothing about storage at all.

Keying the replacement to a configured value keeps the protection where it
belongs: a practitioner asking for a different class still gets the
replacement, because the alternative is an apply that reports success while
every existing volume stays on the old class.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Making the blocks Optional+Computed so the platform's effective values
could be read off the resource was the wrong trade. Terraform copies prior
state into the plan wherever the configuration is silent, so those values
came back as the plan and the next update wrote them into the release as
explicit keys — the cluster froze on the Talos release and schematic that
were current that day, with nothing in the plan to show it. Reading the
request from the configuration instead fixed the freeze but broke the
opposite motion: deleting a pinned line left the plan asserting the old
value while the apply sent nothing, and Terraform failed the apply with an
inconsistent-result error telling the operator to report a provider bug.

Both symptoms come from asking one attribute to be two things. The
resource now tracks only what the configuration sets: unset fields stay out
of the request and out of state, so the platform's default applies and
keeps moving with the platform, and a configured field still refreshes, so
drift against it is still planned away. The data source is where the
effective values — including the ones nobody configured — are read; it
keeps reporting the whole surface, because that is what a data source is
for.

This drops the plan/config divergence entirely, so the hook added to the
shared Create/Update path goes away with it and the engine is back to
expanding the plan.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The presence-preserving section of spec.go exists to pin the difference
between an absent key and an empty value, and every helper in it has a test
saying so — except the two JSON-document ones, whose only exercise was the
populated path through a kubernetes test.

The invalid-document branch is reachable from configuration, not just
defensive: `extra_volumes = [null]` decodes to a null value whose string
form is empty, and the unmarshal fails. The diagnostic named the spec key
but not the element, so a practitioner with six volumes got "unexpected end
of JSON input" and nowhere to look; it now names the index.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The attribute defaulted to "replicated" in the provider, which meant any
cluster running on another class with a configuration that stays silent
about storage got its plan pulled back to "replicated". The server accepts
that write — it does not evaluate the upstream immutability rule — and the
PersistentVolumeClaims stay where they are, so state and reality part ways
for good. Dropping the default closes the path: an unset attribute leaves
the key out and the platform supplies "replicated" itself, and the
plan keeps whatever the cluster already runs on.

Two more cases of the same rule, that a key nobody wrote should not be
sent. A node group with `roles = []` now reaches the server as an empty
list instead of an omitted key; collapsing the two made the read return
null against a plan holding an empty list, which fails the apply on a
perfectly valid configuration. And the control-plane block now says plainly
that configuring it rewrites the whole control-plane section, so the
sizing and replica count it does not model go back to platform defaults —
those cannot be named from the configuration, so the usual "name it to keep
it" escape does not exist there.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Three small corrections and one test.

The storage_class comment claimed the key stays out for the life of the
resource; it stays out on create, and on later applies the plan holds what
the last read reported and writes it back — a no-op, but the comment said
something else. keepConfiguredAttributes returned a configured value
verbatim when the server did not report the block; on a 1.6 cluster that
cannot happen, but against an older one it would put a possibly-unknown
value into state, so an unreported attribute now trims to null like any
other. And oidc.custom_config.secret_ref exists to name a Secret, so
`name` is required inside it rather than silently expanding to nothing.

The new round trip sends the full model through expand and back through
flatten. Expand's keys are guarded against the upstream json tags, but
flatten's are hand-written in both the code and the tests, so a matched
typo on both sides would have passed everything else here.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
`node_groups = {}` is the configuration upstream defaults to and documents:
the chart renders a single md0 that provisions nothing until an
unschedulable Pod brings the autoscaler in. The provider could not express
it. The attribute is required, so the plan holds an empty map, while the
shared map flatten collapses an empty map to null — the apply failed with
an inconsistent-result error on a configuration the platform supports, and
the provider's own example used it.

An empty map now reads back as an empty map, while an absent key still
reads as null, since that is a server that did not report the field rather
than a cluster with no groups.

While trimming the platform blocks, the null built for an attribute the
server left out now comes from the attribute's own type instead of a switch
over the shapes those blocks happen to use today; a bool or a number added
to one of them would otherwise have produced a null string and panicked.

The acceptance test grows a step for the empty-list contract. Whether an
explicitly empty list survives the round trip is a question only a real
server can answer — the unit tests echo the provider's own request back.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The description promised behaviour the chart does not implement: that an
unset key auto-enables a Gateway for a tenant whose apex derives from its
parent, and disables it for a custom apex. The chart's helper resolves a
missing key to false, unconditionally, and never reads `host` — the tenant
simply gets no Gateway of its own and inherits its nearest ancestor's,
falling back to Ingress if no ancestor owns one. The custom-apex tenant is
the one that must ask for `true`, because the ancestor's certificate does
not cover its apex. The wrong story came from upstream's own field comment,
which the provider had copied verbatim.

The encoding is unchanged and still correct: absent and false deploy the
same thing today, but the chart branches on the key being missing rather
than on a null, and the absence is what records that nobody chose.

Two more places where the words claimed more than the code does. The
control-plane block said out-of-band values are lost "once this block is in
play"; the provider replaces the whole spec on every update, so they are
lost either way. And an acceptance step described coverage for an empty
node-group map that it did not have — it has it now, as its own step,
because the map is exactly the case the unit tests cannot decide.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Upstream carries three different stories about this key. The field comment
in the pinned api module says an unset key auto-enables the Gateway for a
derived apex; the chart's own test suite says false is an opt-out that
parks the tenant off its ancestor's Gateway while unset inherits; and the
templates in that same release resolve both to the identical render, since
the helper collapses absent and false before namespace.yaml ever reads it.

The last statement is the one with a mechanism behind it, but it is a
statement about today's templates, not about the contract — and betting a
practitioner's tenant on it is what the previous wording did. The
description now says what the provider does and what upstream means by each
state, and steers toward leaving the key absent rather than writing false,
which is the choice that cannot be wrong under either reading.

Three smaller corrections in the same pass. The kubernetes API-server
passthrough reserves `authentication-config` too, whenever OIDC is on, and
the chart fails the render on it. The tenant data source called the
inherited Gateway a per-tenant controller. And the flagship kubernetes
example pinned storage_class to the value the schema had just stopped
defaulting — pinning it is now the one thing that makes a later change a
cluster replacement, which is not what a first example should teach. The
empty-list acceptance step also covers oidc.users now, the other list whose
empty form the provider claims to preserve.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Upstream says three different things about this key. The generated field
comment says an unset key auto-enables the Gateway for a derived apex; a
chart test comment says false parks the tenant off its ancestor's Gateway;
the helper that actually decides says neither. Two rewrites of this
paragraph have now tracked two of those stories, so this one follows the
helper and nothing else.

tenant.gatewayEffective returns true only for an explicit true, and its own
header states what false means: the tenant does not skip Gateway routing,
it attaches its published Routes to the nearest ancestor that owns a
Gateway, exactly as ingress already inherits. So ownership is what the flag
buys, and in this release false and an absent key buy the same nothing.
The description now says that, and says which one this provider sends.

The rest of the pass: the schema said out-of-band values are dropped after
an import, when the provider replaces the whole spec on every update and
drops them whichever way they were set; the resource flatten paired its
blocks across two parallel slices, where a sixth block added to one and not
the other would trim against its neighbour; and the data-source binding —
the surface behind the documented talos.version output — had no test.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The oidc.users description said an empty list "is not the same as leaving
the attribute unset", and two comments said the same of a node group's
roles. On the cluster they are the same: the chart ranges over both lists,
and the platform defaults users to empty, so neither can tell an empty list
from a missing key. What the empty list actually buys is on the Terraform
side — the plan holds an empty list, so the read has to return one.

The group's genuinely presence-sensitive keys are maxUnhealthy and
nodeStartupTimeout, which the chart reads with hasKey and falls back to the
cluster-wide values for. That is where the argument holds, and where it now
lives.

Also spelled out, next to the snapshot it depends on, that the resource
flatten captures the configured blocks before the embedded flatten
overwrites them — the ordering is the mechanism, and a refactor that moved
one line past the other would invert it silently.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
@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: 7e66b7f0-20d3-424e-b576-dc708376a2f6

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

LGTM with non-blocking notes

The provider code is correct and internally consistent, the no-provider-default design is the right shape for the roll-with-the-platform fields and is well unit-tested; the only residual risks are two server-side behaviours that cannot be proven without a live 1.6 cluster (which the PR already defers), plus an inherited-red base.

Caveats

  • Base branch is red, but this PR adds nothing to it. At the merge-base 583e1a9 (the v1.4.3 -> v1.6.1 api bump, the tip of feat/cozystack-1.6), TestNatsExpandKeysMatchConfigSpec, TestQdrantExpandKeysMatchConfigSpec, TestPostgresqlExpandKeysMatchConfigSpec, TestKafkaExpandKeysMatchConfigSpec already fail with ConfigSpec has "tls" but expand does not emit it. Verified by checking out 583e1a9 and running those four guards: identical failures, and this PR's diff touches none of the four *_model.go files nor tls. They are owned by the sibling tls PR (feat/cozystack-1.6-tls). Zero new failures from this PR, but the integration branch stays red until the tls PR lands.

  • Two correctness properties are server-side and were not (could not be) executed here; both are unit-tested against synthetic server responses but rest on how the live aggregated apiserver actually behaves:

    • Block-trimming vs pinning (keepConfiguredAttributes, kubernetes_model.go:139): the design assumes the aggregated apiserver materialises the talos/nodeHealthCheck/oidc/controlPlane/images defaults on read and that trimming to configured fields keeps them out of state. TestKubernetesResourceFlatten_KeepsUnconfiguredBlocksOut/_RefreshesConfiguredFieldsOnly/_TrimsNestedObjects exercise the flatten against a server view that includes materialised values, so the provider side is verified; what remains unverified is that the real server materialises exactly that shape.
    • Gateway three-state (tenant_model.go:77-105): relies on the tenant apiserver NOT materialising a gateway default on the create response — otherwise a first apply with gateway unset would flatten to false against a null plan and fail with "provider produced inconsistent result after apply". TestFlatten_GatewayAbsentStaysNull proves the provider maps an absent key to null, but not that the server ever returns it absent. This is the exact class the four docs(tenant) commits wrestled with; presence-reliance is correct only if the tenant chart reads gateway with hasKey and does not default it.

Recommended follow-ups

  • Run the new acceptance steps (the update-does-not-pin case and the empty-collection cases) against a live 1.6 cluster before the integration PR reaches master; they are the authoritative check for both server-side assumptions above and are the only unchecked box in the PR's own Testing section.
  • controlPlane.apiServer extra_volumes/extra_volume_mounts and oidc.users are refreshed from the server value on a configured block (keepConfiguredAttributes takes fromServer for a non-null list). If the aggregated apiserver defaults fields inside those free-form passthrough objects, the refreshed value could diverge from config and surface as inconsistent-result-after-apply or perpetual drift. jsontypes.Normalized absorbs whitespace/key-order but not added keys. Worth an explicit acc assertion on round-trip of a populated extra_volumes.
  • Consider marking oidc.custom_config.config (kubernetes_schema.go:243) Sensitive: true for defence-in-depth. An apiserver AuthenticationConfiguration normally carries no secret (issuer, audiences, claim mappings, CA), so this is optional, but kubeconfig is already Sensitive and an inline config is a plausible place for a tenant to paste a bearer.
  • Dropping v1.30 from the version validator (kubernetes_schema.go:439) is a plan-time breaking change for anyone whose config literally names version = "v1.30"; it is correct for a 1.6-targeting provider and documented in the PR body's Breaking-changes section, but the commit/PR title carry no Conventional-Commits !. Confirm the consolidated integration PR to master carries the breaking marker.

Checked and correct

  • storage_class genuinely wires RequiresReplaceIfConfigured() (kubernetes_schema.go:430), not just prose; provider-side replicated default removed; TestKubernetesStorageClassRequiresReplace and TestKubernetesExpand_StorageClassOmittedWhenUnset cover it. client.Update does a full spec replace (internal/client/application.go:121, unchanged by this PR), so the whole-spec-replace behaviour the schema description documents is real.
  • Resource uses the trimming flatten (kubernetesResourceModel), data source uses the full flatten (kubernetesModel); confirmed at provider.go:248,289. Method shadowing over the embedded model is correct.
  • node_groups = {} and roles = [] regressions have targeted tests (TestKubernetesFlatten_EmptyNodeGroupsStayEmpty, TestKubernetesExpand_NodeGroupRolesThreeStates, TestKubernetesFlatten_NodeGroupRolesKeepEmptyList); the presence-preserving helpers in spec.go back them.
  • cpu/memory both-or-neither is a plan-time AlsoRequires pair (kubernetes_schema.go:79-99), tested by TestKubernetesNodeGroupResourcesRequireBothOrNeither.
  • The *ExpandKeysMatchConfigSpec / *KeysMatch*Spec drift guards are a strong regression harness against the pinned api module and are exactly what surfaces the tls gap.
  • go build ./... and go vet ./internal/provider/... clean; docs and examples regenerated and cover every new block.

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