Skip to content

fix(update): only offer Kubernetes upgrades the distribution has promoted - #6487

Merged
devantler merged 2 commits into
mainfrom
claude/k3s-promoted-versions-6486
Aug 5, 2026
Merged

fix(update): only offer Kubernetes upgrades the distribution has promoted#6487
devantler merged 2 commits into
mainfrom
claude/k3s-promoted-versions-6486

Conversation

@devantler

Copy link
Copy Markdown
Contributor

🤖 Generated by the Agentic Engineer

Why

ksail cluster update was recreating people's K3s clusters onto a Kubernetes version that k3s had not released.

k3s publishes its container image the moment a release is tagged, but only promotes that release to its update channels later — sometimes much later. KSail read the image registry's tag list as though it were the list of released versions, so a tag appearing upstream was immediately treated as an available upgrade. On 2026-08-04 k3s pushed v1.36.3-k3s1 without promoting it, and from that moment ksail cluster update deleted and recreated any K3s cluster it was pointed at — no configuration change from the user, and a destructive operation. It also turned every K3s System Test red.

What

Version discovery now only offers versions the distribution has actually promoted, read from k3s' own release-channel index — the same source the k3s installer uses. Distributions with no promotion source behave exactly as before, and if the channel lookup fails KSail declines to upgrade rather than falling back to raw registry tags, so the check cannot quietly stop protecting anything.

Verified against the live k3s index: v1.36.2-k3s1 (promoted) is offered, v1.36.3-k3s1 (not promoted) is not.

Fixes #6486

…oted

An OCI registry serves an image the moment its tag is pushed. k3s pushes
rancher/k3s:<tag> at tag time and promotes the release to its channels
only later, so the registry tag list is not a list of released versions.

Version discovery treated every tag as an upgrade candidate, so within
hours of k3s pushing v1.36.3-k3s1 -- which k3s had not promoted, and
still has not -- 'ksail cluster update' recreated K3s clusters onto it
with no configuration change from the user. Cluster recreation is
destructive, and every K3s System Test failed on the same assertion.

Constrain discovery to versions the distribution has actually promoted,
read from k3s' own channel index. Images with no registered promotion
source pass through unchanged, and a broken source fails closed rather
than falling back to raw tags.

Fixes #6486
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Verification record

RED/GREEN with a control on the ablation. With FilterPromoted ablated to return versions:

  • the ablated tree still builds (so the failure is behavioural, not a syntax break that would certify nothing),
  • TestComputeUpgradePath_SkipsUnpromotedK3sReleaseFAIL (the defect returns),
  • TestComputeUpgradePath_UsesPromotedK3sReleasePASS and TestK3sChannelSource_PromotedTagsPASS (the ablation changed exactly one behaviour, not everything).

Restored, all tests pass.

Exercised against the real endpoint, not a fixture: NewK3sChannelSource().PromotedTags() against https://update.k3s.io/v1-release/channels returned 22 promoted tags, with v1.36.2-k3s1 present and v1.36.3-k3s1 absent — the exact discrimination that stops the recreation. The probe was removed before commit.

Cross-checked against three independent sources agreeing that v1.36.3 is not released: the channel index (stable and latest both v1.36.2+k3s1), repos/k3s-io/k3s/releases/latest (v1.36.2+k3s1), and the GitHub release flag on v1.36.3+k3s1 (prerelease=true).

End-to-end proof is this PR's own K3s System Tests. They are the jobs that were failing on the assertion; passing here is the behavioural confirmation, so I am waiting on them rather than claiming the fix works from the unit tests.

Local lint note — pre-existing, not introduced here. golangci-lint reports G704: SSRF via taint analysis on the client.Do(req) in the new channel source. I ran untouched packages as a control: the same rule fires on pkg/svc/installer/cni/cilium/gateway_api.go:179 and pkg/svc/installer/localpathstorage/installer.go:105, both already on main with main green. The new code follows the same shape as that existing precedent, and the endpoint is a package constant rather than a settable field, so there is no new taint surface.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

MegaLinter analysis: Success

✅ Linters with no issues

actionlint, bash-exec, git_diff, hadolint, jscpd, jsonlint, lychee, markdown-table-formatter, markdownlint, prettier, prettier, shellcheck, shfmt, stylelint, syft, trivy-sbom, trufflehog, v8r, v8r, yamllint

Notices

📣 MegaLinter 9.5.0 is out! Discover the new features and security recommendations in the release announcement. (Skip this info by defining SECURITY_SUGGESTIONS: false)

See detailed reports in MegaLinter artifacts

MegaLinter is graciously provided by OX Security
Show us your support by starring ⭐ the repository

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

@devantler I will review the changes in #6487.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds a K3s channel source that retrieves promoted release versions and converts them to OCI tags. A promotion-aware resolver filters registry versions for registered image sources and propagates channel errors. Unknown image sources retain their base resolver results. Cluster reconciliation and version-drift calculations now use the promotion-aware resolver. Tests cover promoted and unpromoted K3s releases, passthrough behavior, error handling, channel parsing, deduplication, and empty responses.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement promotion-aware K3s version discovery, fail closed on lookup errors, and apply consistently to upgrade resolution and drift checks.
Out of Scope Changes check ✅ Passed All code and tests directly support promotion-aware version filtering and protection against unpromoted K3s upgrades.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly identifies the change to restrict Kubernetes upgrades to distribution-promoted versions.
Description check ✅ Passed The description explains the K3s promotion issue, the implemented resolver behavior, and the safety outcome.

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.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/svc/versionresolver/k3schannel.go`:
- Around line 92-98: The K3s channel source currently treats every channel’s
Latest value as promotion evidence; update K3sChannelSource in
pkg/svc/versionresolver/k3schannel.go (lines 92-98) to include only
stable-channel releases, excluding tags present solely in latest or
minor-version channels. Extend the promotion test in
pkg/svc/versionresolver/promotion_test.go (lines 169-174) with a non-stable-only
tag and assert that K3sChannelSource excludes it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 36d3adfa-07e8-4e2c-b260-1cd0003d911e

📥 Commits

Reviewing files that changed from the base of the PR and between 2183721 and 266db5d.

📒 Files selected for processing (5)
  • pkg/cli/cmd/cluster/orchestrator.go
  • pkg/cli/cmd/cluster/version_drift.go
  • pkg/svc/versionresolver/k3schannel.go
  • pkg/svc/versionresolver/promotion.go
  • pkg/svc/versionresolver/promotion_test.go
📜 Review details
⏰ Context from checks skipped due to timeout. (5)
  • GitHub Check: 🧪 System Test (Docker) (Talos, Docker, true, --name system-test-cluster-with-image-verificatio...
  • GitHub Check: 🧪 System Test (Docker) (Talos, Docker, true, --gitops-engine Flux --local-registry ghcr.io/dev...
  • GitHub Check: 🧪 System Test (Docker) (Talos, Docker, true)
  • GitHub Check: 🧪 System Test (Docker) (Talos, Docker, true, --name system-test-cluster --cni Cilium --csi Ena...
  • GitHub Check: 🧪 System Test (Docker) (Talos, Docker, true, --cni Calico --csi Disabled --load-balancer Disab...
🧰 Additional context used
📓 Path-based instructions (4)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Use Go 1.26.1 or newer, matching the version declared in go.mod.
All user-supplied file path arguments in CLI commands must be canonicalized with fsutil.EvalCanonicalPath before use; create parent directories first for new output paths.
Use fsutil.ReadFileSafe for constrained file reads instead of reimplementing path-containment checks.
Do not manually register MCP or Copilot tool handlers; runnable Cobra commands are exposed through automatic generation in pkg/toolgen.
Use a typed experimental field in ksail.yaml for configuration-gated behavior that is not an entire command; regenerate the schema and CRD.
Graduate validated experimental features by deleting the single Guard call; do not retain unnecessary experimental scaffolding.
Run formatting and linting with golangci-lint run --fix and golangci-lint run --timeout 5m; validate with go build and go test ./....

Files:

  • pkg/cli/cmd/cluster/orchestrator.go
  • pkg/svc/versionresolver/k3schannel.go
  • pkg/cli/cmd/cluster/version_drift.go
  • pkg/svc/versionresolver/promotion_test.go
  • pkg/svc/versionresolver/promotion.go
pkg/cli/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

pkg/cli/**/*.go: New not-yet-stable commands must be wrapped with experimental.Guard(cmd), remain disabled by default, and require the global --experimental flag.
Test experimental commands in both states: enabled with --experimental and disabled with experimental.ErrDisabled.

Files:

  • pkg/cli/cmd/cluster/orchestrator.go
  • pkg/cli/cmd/cluster/version_drift.go
**/*.{go,yaml,yml,md,mdx,ts,tsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Generated files must not be hand-edited; run make generate as the canonical regeneration command.

Files:

  • pkg/cli/cmd/cluster/orchestrator.go
  • pkg/svc/versionresolver/k3schannel.go
  • pkg/cli/cmd/cluster/version_drift.go
  • pkg/svc/versionresolver/promotion_test.go
  • pkg/svc/versionresolver/promotion.go
**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

Add regression tests for confident bug fixes and run flaky-test candidates repeatedly with go test -run <T> -count=10 ./....

Files:

  • pkg/svc/versionresolver/promotion_test.go
🧠 Learnings (1)
📚 Learning: 2026-08-02T19:26:41.922Z
Learnt from: devantler
Repo: devantler-tech/ksail PR: 6434
File: pkg/cli/clusterapi/eks_create_identity_test.go:0-0
Timestamp: 2026-08-02T19:26:41.922Z
Learning: In Go tests using Testify v1.11.1, do not flag require.NoError(t, err) inside an Eventually condition solely because the condition may run in another goroutine: require calls t.Errorf before FailNow, and t.Errorf marks the outer test as failed. However, prefer decomposing Eventually conditions so service errors, missing resources, and state or phase mismatches are reported separately for clearer diagnostics.

Applied to files:

  • pkg/svc/versionresolver/promotion_test.go
🔇 Additional comments (5)
pkg/svc/versionresolver/k3schannel.go (1)

1-90: LGTM!

Also applies to: 100-115

pkg/svc/versionresolver/promotion_test.go (1)

1-162: LGTM!

Also applies to: 176-216

pkg/svc/versionresolver/promotion.go (1)

1-91: LGTM!

pkg/cli/cmd/cluster/orchestrator.go (1)

179-179: LGTM!

pkg/cli/cmd/cluster/version_drift.go (1)

54-54: LGTM!

Comment thread pkg/svc/versionresolver/k3schannel.go
k3s publishes "testing" channels alongside the stable ones, and their
latest names a release candidate rather than a release. Live index on
2026-08-05: both testing and v1.18-testing point at v1.36.2's stream
predecessor v1.18.2-rc3+k3s1.

Unioning every channel's latest therefore admitted that rc as promotion
evidence, so a cluster on v1.18.x could be offered an upgrade onto a
release candidate — the same class of unreleased-version upgrade this
source was added to prevent.

Filter on the version's own semver pre-release segment rather than the
channel name, so a differently-named pre-release channel is caught too.
Minor-stream channels (v1.18 -> v1.18.20+k3s1, v1.34 -> v1.34.9+k3s1)
name real releases and are deliberately still admitted; restricting to
the stable channel alone would strand every cluster not on the newest
minor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

@devantler I will review the changes in #6487.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Readiness record — head cb92c05a8

Reviewed. CodeRabbit at cb92c05a81, COMMENTED, zero finding sections, zero unresolved threads. Its one 🔴 Critical on the previous head was valid in substance and is fixed; the reasoning for diverging from its proposed remedy is in the resolved thread.

Tried and evaluated as a user — exercised against the live k3s index, not fixtures:

LIVE ok: v1.36.3-k3s1     correctly excluded   <- the bug this PR fixes (#6486)
LIVE ok: v1.18.2-rc3-k3s1 correctly excluded   <- release candidate, CodeRabbit's finding
LIVE ok: v1.36.2-k3s1     present              <- stable still offered
LIVE ok: v1.18.20-k3s1    present              <- minor stream still offered
LIVE ok: v1.34.9-k3s1     present              <- minor stream still offered

Both directions matter: the check has to stop offering unreleased versions without stranding clusters that are not on the newest minor.

Programmatically tested. RED before / GREEN after on TestK3sChannelSource_ExcludesPreReleases; full versionresolver package green. The local G704 gosec warning on k3schannel.go:77 is pre-existing on the line this PR does not touch — Lint - golangci-lint passed at the previous head with that same line present, so it is local-config divergence, not a regression.

Scope note for the maintainer — the released CLI is affected

Worth knowing when deciding how fast this ships:

  • latest release v7.178.14 — 2026-08-04 08:11Z
  • k3s pushed the unpromoted v1.36.3-k3s1 tag — 2026-08-04 19:42Z

The release predates the trigger, so every released version carries the old behaviour. The consequence is not cosmetic: on a K3s cluster, ksail cluster update reads the registry tag as an available upgrade, decides recreation is required, and deletes and recreates the cluster — with no configuration change from the user. CI is where we noticed it, but users are exposed to the same path today.

That argues for a release once this merges rather than letting it ride to the next scheduled one.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Correction — the pre-release fix is defense-in-depth, not a live user-facing bug

I checked reachability after the fact and I overstated the CodeRabbit finding. Correcting it before it informs a release decision.

What is still exactly as described: the original defect (#6486). v1.36.3-k3s1 carries no pre-release marker, so it is a "stable" version by every downstream filter — the promotion check is genuinely the only thing that stops it. The destructive cluster update behaviour and the released-version exposure in my earlier comment stand unchanged.

What I got wrong: I implied the release-candidate leak was equally reachable. It is not. ComputeUpgradePath calls FilterStable(allVersions) before intersecting with the promoted set, and ParseVersion classifies v1.18.2-rc3-k3s1 as a pre-release. So the rc was dropped downstream regardless of what PromotedTags returned — it could not have been offered to a user.

So CodeRabbit's finding is accurate about PromotedTags (the RED proof is real — the union did admit the rc) but the consequence is latent, not live. I should have traced the call path before calling it "the same class of defect".

The change stays, on its merits rather than on urgency: an allow-list named PromotedTags should contain only promoted releases. Leaving an rc in it makes correctness depend on a filter three call-frames away — fine today, and a trap the moment a caller uses PromotedTags directly or FilterStable changes. The regression test pins both directions.

Net effect on merge urgency: unchanged — driven by #6486, which is real and destructive.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

This bug is now reddening unrelated PRs across the repo

Evidence gathered while this PR was waiting on CI. Two unrelated open PRs fail on the same K3s
legs, with the same message, and neither touches Kubernetes version selection:

Both fail identically at system-test.cluster-update:

ERROR: Unexpected changes detected during update after cluster creation
This indicates a regression — the cluster was just created with the same configuration.

In #6489's run the cluster was created with rancher/k3s:v1.36.3-k3s1 and cluster update
immediately wanted to change it — which is exactly the unpromoted-release selection this PR fixes.

That makes a fairly clean natural experiment: three PRs, three different diffs, one shared failure,
and the only common factor is main. So this is not a per-PR flake — it is the released behaviour,
and it is currently costing every PR in the repo a red CI leg on top of the user-facing impact
(ksail cluster update deletes and recreates a K3s cluster onto an unpromoted version, with no user
config change).

Unrelated to the above: this PR's own one red leg was the traefik/whoami export flake, not this
diff — it passed on re-run. Root-caused separately in #6488 and fixed in #6489.

@devantler
devantler marked this pull request as ready for review August 5, 2026 03:58
@devantler
devantler merged commit 4e3c630 into main Aug 5, 2026
117 of 119 checks passed
@devantler
devantler deleted the claude/k3s-promoted-versions-6486 branch August 5, 2026 03:59
@github-project-automation github-project-automation Bot moved this from 🫴 Ready to ✅ Done in 🌊 Project Board Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

cluster update upgrades K3s clusters to releases k3s has not promoted

1 participant