Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package k0sctl for openSUSE:Factory checked 
in at 2026-06-18 18:44:38
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/k0sctl (Old)
 and      /work/SRC/openSUSE:Factory/.k0sctl.new.1981 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "k0sctl"

Thu Jun 18 18:44:38 2026 rev:22 rq:1360249 version:0.31.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/k0sctl/k0sctl.changes    2026-05-11 
17:06:19.378967254 +0200
+++ /work/SRC/openSUSE:Factory/.k0sctl.new.1981/k0sctl.changes  2026-06-18 
18:45:32.433352640 +0200
@@ -1,0 +2,19 @@
+Thu Jun 18 12:20:02 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 0.31.0:
+  * Handle unexpected names in the kubeconfig (#1087)
+  * Bump github.com/k0sproject/rig from 0.21.8 to 0.21.10 (#1104)
+  * Bump k8s.io/client-go from 0.36.1 to 0.36.2 (#1097)
+  * Bump k8s.io/apimachinery from 0.36.1 to 0.36.2 (#1098)
+  * Enable testifylint (#1102)
+  * Don't autodetect CPLB IP as host private address (#1101)
+  * Fix unnecessary reinstall when dynamicConfig is enabled (#1099)
+  * Bump golang.org/x/text from 0.37.0 to 0.38.0 (#1091)
+  * Bump github.com/go-playground/validator/v10 from 10.30.2 to
+    10.30.3 (#1090)
+  * Bump github.com/k0sproject/rig from 0.21.6 to 0.21.8 (#1089)
+  * Bump k8s.io/client-go from 0.36.0 to 0.36.1 (#1083)
+  * Bump golang.org/x/text from 0.36.0 to 0.37.0 (#1082)
+  * Update AGENTS.md
+
+-------------------------------------------------------------------

Old:
----
  k0sctl-0.30.1.obscpio

New:
----
  k0sctl-0.31.0.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ k0sctl.spec ++++++
--- /var/tmp/diff_new_pack.naQi17/_old  2026-06-18 18:45:34.477437926 +0200
+++ /var/tmp/diff_new_pack.naQi17/_new  2026-06-18 18:45:34.485438260 +0200
@@ -18,7 +18,7 @@
 
 
 Name:           k0sctl
-Version:        0.30.1
+Version:        0.31.0
 Release:        0
 Summary:        A bootstrapping and management tool for k0s clusters
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.naQi17/_old  2026-06-18 18:45:34.717447940 +0200
+++ /var/tmp/diff_new_pack.naQi17/_new  2026-06-18 18:45:34.749449275 +0200
@@ -2,7 +2,7 @@
   <service name="obs_scm" mode="manual">
     <param name="url">https://github.com/k0sproject/k0sctl.git</param>
     <param name="scm">git</param>
-    <param name="revision">v0.30.1</param>
+    <param name="revision">v0.31.0</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.naQi17/_old  2026-06-18 18:45:34.897455450 +0200
+++ /var/tmp/diff_new_pack.naQi17/_new  2026-06-18 18:45:34.933456952 +0200
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param 
name="url">https://github.com/k0sproject/k0sctl.git</param>
-              <param 
name="changesrevision">4afdb2b1bf3f10f57beddf00a85b72a0a0000927</param></service></servicedata>
+              <param 
name="changesrevision">def1a095778cd5ca0ef703d9b6fe13789b17f67a</param></service></servicedata>
 (No newline at EOF)
 

++++++ k0sctl-0.30.1.obscpio -> k0sctl-0.31.0.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/k0sctl-0.30.1/.golangci.yml 
new/k0sctl-0.31.0/.golangci.yml
--- old/k0sctl-0.30.1/.golangci.yml     2026-05-08 13:16:19.000000000 +0200
+++ new/k0sctl-0.31.0/.golangci.yml     2026-06-18 13:20:25.000000000 +0200
@@ -1,6 +1,9 @@
 version: "2"
 
 linters:
+  enable:
+    - testifylint
+
   settings:
     errcheck:
       exclude-functions:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/k0sctl-0.30.1/AGENTS.md new/k0sctl-0.31.0/AGENTS.md
--- old/k0sctl-0.30.1/AGENTS.md 2026-05-08 13:16:19.000000000 +0200
+++ new/k0sctl-0.31.0/AGENTS.md 2026-06-18 13:20:25.000000000 +0200
@@ -1,97 +1,82 @@
 # Repository Guidelines
 
-## Project Structure & Module Organization
-- Root module: `github.com/k0sproject/k0sctl` (Go, aiming to keep version up 
to date).
-- Source layout:
-  - `main.go`: entrypoint for the CLI.
-  - `cmd/`: CLI commands/subcommands (e.g., `apply.go`, `init.go` which map to 
commands like "k0sctl apply", "k0sctl init").
-  - `phase/`: orchestration phases for cluster operations and the phase 
manager that runs them.
-  - `pkg/`: reusable packages (e.g., `k0sfeature/`, `manifest/`).
-  - `internal/`: internal helpers (e.g., `shell/`).
-  - `smoke-test/`: end‑to‑end smoke tests (Make targets, uses 
github.com/k0sproject/bootloose for test machines).
-  - `examples/`: somewhat outdated examples for Terraform use.
-
-## What The App Does
-- Bootstraps and manages k0s Kubernetes clusters by connecting to nodes over 
SSH using github.com/k0sproject/rig as the SSH driver and target OS/distro 
compatibility layer.
-- Core operations: "apply" connects to hosts, installs k0s, configures, and 
starts it; "reset" uninstalls k0s and cleans up; "init" creates a sample 
config, "kubeconfig" generates a config to access the cluster using kubectl.
-- Input: `k0sctl.yaml` describing hosts, roles, and k0s version/config; 
output: actions executed remotely with clear phase logs.
-- `urfave/cli` is used as the CLI framework.
-
-## Build, Test, and Development Commands
-- Build local binary: `make k0sctl` (outputs `./k0sctl`).
-- Cross‑builds: `make build-all` (artifacts in `bin/`).
-- Run unit tests: `make test` or `go test -v ./...`.
-- Lint: `make lint` (uses golangci-lint defaults).
-- Smoke tests (CI matrix in `.github/workflows/smoke.yml`): run locally with 
targets like `make smoke-basic`, `make smoke-upgrade`. Requires a working 
`bootloose` (k0sproject/bootloose) setup and virtualization/container tooling; 
commonly set `LINUX_IMAGE` env.
-- Run locally: `go run .` or `./k0sctl --help` after build.
-
-## Coding Style & Naming Conventions
-- Follow standard Go style (`gofmt`, `goimports`), tabs for indentation.
-- Package names: short, lowercase; files use `snake_case.go`; tests 
`*_test.go`.
-- Keep CLI flags/env vars consistent with `cmd/` patterns.
-- Use latest Go or at least the version in `go.mod` (toolchain pinned).
-
-## Testing Guidelines
-- Unit tests: colocate `*_test.go`; use Go `testing`/`testify` as needed.
-- Coverage: no minimum enforced; run `go test -cover ./...` locally.
-- Smoke tests: mirror CI matrix in `.github/workflows/smoke.yml` (e.g., 
`smoke-basic`, `smoke-upgrade`, `smoke-reset`, `smoke-backup-restore`). Run 
locally if you have `bootloose` working.
-- Name tests after behavior (e.g., `TestValidateHosts_*`).
- - Transport in tests: prefer the `ssh` transport (Go `crypto/ssh`) for 
end-to-end/smoke coverage; only use `openSSH` when explicitly testing that 
transport mode.
- - Unit tests must not connect to real hosts or change remote state. Prefer 
`localhost` connections (Rig’s localhost driver) or mocks/fakes for execution 
and file transfer.
- - Keep tests hermetic and deterministic; avoid relying on external network 
resources, time-based flakiness, or host-specific configuration.
-
-## Commit & Pull Request Guidelines
-- Commits: concise, imperative; keep history clean by rebasing/editing instead 
of piling “fix typo” commits. Multiple well‑maintained commits welcome but 
maintainers often squash on merge.
-  - Example: `Fix panic when parsing multi-doc YAML`.
-- PRs: include problem statement, approach, impact; link issues; include 
sample config/output when changing CLI or phases.
-- CI: All checks must pass (lint, unit, smoke). Update `README.md` (source of 
truth) when config fields or behavior change; update `examples/` if applicable.
-
-## Security & Compliance
-- DCO required: sign off commits (`git commit -s`) with `Signed-off-by: Name 
<email>`.
-- Examples and tests must only use private addresses and redacted or 
test-generated key files.
-- Prefer the `openSSH` transport or SSH agent usage over embedding private 
keys in configs.
-
-## Architecture Overview
-- CLI in `cmd/` maps subcommands to actions in action/ which compose phases 
and hand them to `phase.Manager`.
-- Config flow: parse `k0sctl.yaml` → build `v1beta1.Cluster` → defaults via 
`creasty/defaults` → run phases with logging.
-- Dry‑run: `Manager.DryRun=true` records intended actions (`DryMsg`, `Wet`) 
and prints a per‑host plan.
-
-## Phase Manager
-- Contract: `type Phase interface { Run(ctx) error; Title() string }`.
-- Optional interfaces used by the manager:
-  - `withconfig.Prepare(*Cluster) error` for precompute/validation.
-  - `conditional.ShouldRun() bool` to skip dynamically.
-  - `beforehook.Before(title) error` and `afterhook.After(err) error` for 
hooks.
-  - `withDryRun.DryRun() error` for alternate dry‑run behavior.
-  - `withcleanup.CleanUp()` on failure paths; 
`withmanager.SetManager(*Manager)` for access to helpers.
-- Concurrency: `Manager.Concurrency` and `ConcurrentUploads` are respected by 
phases that parallelize work.
-- Naming: phase titles must be human‑readable and describe the intention 
(e.g., `PrepareHosts`, `InstallWorkers`, `UpgradeWorkers`). Files use 
`snake_case.go` matching the title.
-
-## Transport Layer: k0sproject/rig
-- Rig provides connection/execution primitives used by phases: SSH (native) 
and OpenSSH client modes, file transfers, sudo elevation, bastion support, env 
propagation, and structured logging.
-- Windows/WinRM exists in rig but k0sctl targets Linux nodes; SSH is the 
primary transport here. Windows support may be added in the future.
-- Repo: `github.com/k0sproject/rig` (use v0.x). The `main` branch is for the 
future v2 API which k0sctl will eventually migrate to.
- - YAML keys: `ssh:` selects the native Go `crypto/ssh` transport (preferred 
default), `openSSH:` uses the locally installed OpenSSH client, and 
`localhost:` uses the local machine as the target.
-
-## Extending & Adding Phases
-- Implement a new `phase.Phase` in `phase/` with a clear `Title()` and 
idempotent `Run(ctx)`; use optional interfaces where appropriate (Prepare, 
DryRun, hooks).
-- Wire it from the relevant action in `cmd/` by calling 
`manager.AddPhase(...)` in correct order.
-- Use manager helpers for dry‑run (`Wet`, `DryMsg`) and respect concurrency 
where parallel work occurs. Any action that changes remote hosts must be 
wrapped so that no changes are made when `--dry-run` is active.
-- Add unit tests and a focused smoke test target if the behavior impacts 
cluster state; update the CI matrix when adding OS/distro specifics.
-
-## OS/Distro Support
-- See CI matrix in `.github/workflows/smoke.yml` for tested distributions. 
When adding support, extend the matrix and add corresponding smoke tests.
-
-## Dependencies
-- Dependabot is enabled; manual bumps are fine.
-- Main dependencies are `github.com/k0sproject/rig`, `github.com/urfave/cli`, 
`github.com/stretchr/testify`, `github.com/creasty/defaults`, and 
`github.com/sirupsen/logrus`.
-- Rig and k0s are maintained by the same authors.
-
-## Agent Checklist (Compact)
-- Default transport: use `ssh` (Go `crypto/ssh`). Use `openSSH` only when 
testing that mode explicitly. Use `localhost` or mocks in unit tests; avoid any 
real host changes.
-- New phases: make them idempotent, implement `Title() string` and `Run(ctx)`; 
use `Prepare`, `ShouldRun`, hooks, and `DryRun()` when appropriate.
-- Dry-run: wrap mutating calls with `Manager.Wet(...)` and emit `DryMsg` so 
`--dry-run` prints an accurate plan without changing state.
-- Concurrency: respect `Manager.Concurrency` and `ConcurrentUploads` in 
parallel work.
-- Naming/style: file names `snake_case.go`, human-readable phase titles, 
follow Go formatting, keep CLI flags/env vars consistent with patterns in 
`cmd/`.
-- Security: do not embed secrets; prefer SSH agent or `openSSH` over inline 
keys in examples; use private/test addresses in docs/tests.
-- Docs/CI: update `README.md` when behavior/config changes; extend smoke 
matrix and add focused smoke tests for OS/distro changes.
+## Project Shape
+- `main.go` is the CLI entrypoint.
+- `cmd/` defines CLI commands and flags using `urfave/cli/v2`.
+- `action/` contains command-level workflows that compose phases.
+- `phase/` contains orchestration phases and the phase manager.
+- `configurer/` contains OS/distro-specific command and path behavior.
+- `pkg/apis/k0sctl.k0sproject.io/v1beta1/` contains the config API types and 
validation.
+- `pkg/`, `internal/`, `smoke-test/`, `examples/`, and `integration/` contain 
reusable packages, private helpers, smoke tests, examples, and integration 
helpers.
+
+## What k0sctl Does
+- Bootstraps and manages k0s clusters from `k0sctl.yaml`.
+- Connects to hosts primarily over SSH through `github.com/k0sproject/rig`; 
Windows workers may use SSH or WinRM. Rig is maintained by the same team and 
provides a consistent interface for remote execution, file transfer, and 
parallelism.
+- Main flows include `apply`, `reset`, `init`, `kubeconfig`, `backup`, and 
dynamic config edit/status commands.
+- Config flow: parse YAML into `v1beta1.Cluster`, apply defaults, validate, 
then run phases through `phase.Manager`.
+
+## Development Commands
+- Build: `make k0sctl`.
+- Cross-build: `make build-all`.
+- Unit tests: `make test` or `go test -v ./...`.
+- Lint: `make lint`.
+- Smoke tests: inspect `Makefile`, `smoke-test/Makefile`, and 
`.github/workflows/smoke.yml` for current targets and matrix.
+- Use the Go/toolchain version declared in `go.mod`
+
+## Testing Rules
+- Unit tests must be hermetic: do not connect to real hosts, mutate remote 
state, rely on external network resources, or depend on host-specific config. 
Prefer mocks/fakes for unit tests.
+- For end-to-end and smoke coverage, prefer the native `ssh` transport (`ssh:` 
/ Go `crypto/ssh`).
+- Use `openSSH:` only when explicitly testing the OpenSSH-client transport.
+- Name tests after behavior, e.g. `TestValidateHosts_*`.
+
+## Phase Manager Rules
+- New phases must be idempotent, have human-readable titles, and live in 
`snake_case.go` files matching the phase intent. The struct name must be 
`PascalCase` matching the intent.
+- Wire phases from the relevant action by adding them to the manager in the 
correct order.
+- Respect `Manager.Concurrency` and `ConcurrentUploads` when doing parallel 
host work or uploads.
+
+## Phase Implementation
+- Embed `GenericPhase` in every new phase struct — it provides `Prepare`, 
`Wet`, `DryMsg`, `DryMsgf`, `parallelDo`, `parallelDoUpload`, and `SetManager`.
+- Use `p.parallelDo(ctx, hosts, func(ctx, h) error { ... })` for concurrent 
per-host work; it respects `Manager.Concurrency` automatically.
+- Use `p.parallelDoUpload(ctx, hosts, ...)` for file-transfer steps; it 
additionally respects `Manager.ConcurrentUploads`.
+- `Wet(host, dryMsg, mutatingFuncs...)` runs the functions only in wet mode 
and prints `dryMsg` in dry-run mode — use it for every remote mutation.
+
+## Dry-Run Rules
+- Any action that changes remote hosts must be guarded so `--dry-run` makes no 
remote changes.
+- Use `Wet(host, dryMsg, funcs...)` and `DryMsg(host, msg)` (both on 
`GenericPhase`) so dry-run output is an accurate per-host plan.
+- If a phase needs alternate dry-run behavior, implement the dry-run interface 
instead of partially running mutating logic.
+
+## Logging
+- Use `log "github.com/sirupsen/logrus"` — it is the only logger in this 
project.
+- Per-host messages must prefix the host: `log.Infof("%s: doing thing", h)`.
+- Use `log.Debug`/`log.Debugf` for internal state, `log.Info`/`log.Infof` for 
user-visible progress, `log.Warn`/`log.Warnf` for recoverable problems.
+- Do not use `fmt.Print*` for diagnostic output.
+
+## Error Wrapping
+- Wrap errors with context using `fmt.Errorf("doing X: %w", err)`.
+- Do not re-wrap the same message twice up the call stack.
+
+## Config API Changes
+When adding or changing fields in `pkg/apis/k0sctl.k0sproject.io/v1beta1/`:
+1. Update defaults (`creasty/defaults` struct tags or `SetDefaults` methods).
+2. Update `Validate()` if the field has invariants.
+3. Update `README.md` with the field name, type, default, and description.
+4. Add a unit test in the same package.
+
+## Transport And Platform Notes
+- YAML keys: `ssh:` selects the native Go SSH transport, `openSSH:` uses the 
local OpenSSH client, `localhost:` targets the local machine, and `winRM:` is 
for Windows workers using WinRM, but SSH also functions on Windows if 
configured.
+- Linux is the primary target. Windows support is limited to worker nodes and 
requires compatible k0s versions; check `README.md` and config validation 
before changing Windows behavior.
+- Rig v0.x is the dependency line used here. Rig v2 API will be migrated to in 
the future.
+
+## Docs, CI, And Security
+- `README.md` is the user-facing source of truth for config fields and 
behavior. Update it when behavior, flags, or config schema changes.
+- Update examples only when they are relevant to the behavior being changed.
+- DCO is required: commits must be signed off with `git commit -s`.
+- Keep secrets, real cluster addresses, and private keys out of docs, 
examples, and tests. Use private/test addresses and redacted or generated key 
material.
+- Before assuming CI requirements, inspect `.github/workflows/`; workflows 
include more than unit tests and smoke tests.
+
+## Agent Token Discipline
+- Prefer narrow reads first: use `rg`, `git diff --stat`, `git diff 
--name-only`, targeted `sed`, and package-level tests before reading whole 
files, full diffs, or full logs.
+- Keep tool output capped. Start with small `max_output_tokens` values, then 
rerun narrower commands if more context is needed.
+- Avoid dumping raw smoke logs, full PR threads, or full `go test ./...` 
output into context. Search for specific failures, phases, files, or review 
comments instead.
+- Preserve useful discoveries in agent memory, `AGENTS.md`, or a focused note 
before ending a large investigation, so future sessions do not repeat the same 
repo sweep.
+- Start a fresh session after a substantial phase of work, especially after 
long PR review/comment loops, and seed it with only the current branch, failing 
check, unresolved comment, and relevant files.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/k0sctl-0.30.1/go.mod new/k0sctl-0.31.0/go.mod
--- old/k0sctl-0.30.1/go.mod    2026-05-08 13:16:19.000000000 +0200
+++ new/k0sctl-0.31.0/go.mod    2026-06-18 13:20:25.000000000 +0200
@@ -14,7 +14,7 @@
        github.com/creasty/defaults v1.8.0
        github.com/gofrs/uuid v4.4.0+incompatible // indirect
        github.com/k0sproject/dig v0.4.0
-       github.com/k0sproject/rig v0.21.6
+       github.com/k0sproject/rig v0.21.10
        github.com/logrusorgru/aurora v2.0.3+incompatible
        github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // 
indirect
        github.com/masterzen/winrm v0.0.0-20250927112105-5f8e6c707321 // 
indirect
@@ -23,23 +23,23 @@
        github.com/sirupsen/logrus v1.9.4
        github.com/stretchr/testify v1.11.1
        github.com/urfave/cli/v2 v2.27.7
-       golang.org/x/crypto v0.49.0 // indirect
-       golang.org/x/net v0.52.0 // indirect
-       golang.org/x/sys v0.42.0 // indirect
-       golang.org/x/term v0.41.0 // indirect
-       golang.org/x/text v0.36.0
+       golang.org/x/crypto v0.52.0 // indirect
+       golang.org/x/net v0.54.0 // indirect
+       golang.org/x/sys v0.45.0 // indirect
+       golang.org/x/term v0.43.0 // indirect
+       golang.org/x/text v0.38.0
        gopkg.in/yaml.v2 v2.4.0
 )
 
 require (
        al.essio.dev/pkg/shellescape v1.6.0
        github.com/carlmjohnson/versioninfo v0.22.5
-       github.com/go-playground/validator/v10 v10.30.2
+       github.com/go-playground/validator/v10 v10.30.3
        github.com/jellydator/validation v1.2.0
        github.com/k0sproject/version v0.8.0
        github.com/sergi/go-diff v1.4.0
-       k8s.io/apimachinery v0.36.0
-       k8s.io/client-go v0.36.0
+       k8s.io/apimachinery v0.36.2
+       k8s.io/client-go v0.36.2
 )
 
 require (
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/k0sctl-0.30.1/go.sum new/k0sctl-0.31.0/go.sum
--- old/k0sctl-0.30.1/go.sum    2026-05-08 13:16:19.000000000 +0200
+++ new/k0sctl-0.31.0/go.sum    2026-06-18 13:20:25.000000000 +0200
@@ -60,8 +60,8 @@
 github.com/go-playground/locales v0.14.1/go.mod 
h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 github.com/go-playground/universal-translator v0.18.1 
h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1/go.mod 
h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.30.2 
h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
-github.com/go-playground/validator/v10 v10.30.2/go.mod 
h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
+github.com/go-playground/validator/v10 v10.30.3 
h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8=
+github.com/go-playground/validator/v10 v10.30.3/go.mod 
h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc=
 github.com/gofrs/uuid v4.4.0+incompatible 
h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
 github.com/gofrs/uuid v4.4.0+incompatible/go.mod 
h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/google/gnostic-models v0.7.0 
h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
@@ -104,8 +104,8 @@
 github.com/json-iterator/go v1.1.12/go.mod 
h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/k0sproject/dig v0.4.0 
h1:yBxFUUxNXAMGBg6b7c6ypxdx/o3RmhoI5v5ABOw5tn0=
 github.com/k0sproject/dig v0.4.0/go.mod 
h1:rlZ7N7ZEcB4Fi96TPXkZ4dqyAiDWOGLapyL9YpZ7Qz4=
-github.com/k0sproject/rig v0.21.6 
h1:9RqeA02FwAktHQpfKfftl69a3bjpyEKDqkiUxXtETtc=
-github.com/k0sproject/rig v0.21.6/go.mod 
h1:bdRDs2dJngbYAEWd6+irYlzUqqBeVXVBcSelpaIvuCw=
+github.com/k0sproject/rig v0.21.10 
h1:VcozC0Ctwl2Ceoo69LZ4twAPVpzhIjp9qfufmMnyO9s=
+github.com/k0sproject/rig v0.21.10/go.mod 
h1:EgLRaRBorLg7aeYGjj2qISKdpU9oxSJxW0Ccjz3gieE=
 github.com/k0sproject/version v0.8.0 
h1:Yh1SFDeBqQ7etrGwffY8bWKdbAUjeBOhiZ6oQuuz4sM=
 github.com/k0sproject/version v0.8.0/go.mod 
h1:iNV3O8blndsQhxZ8zACfpQhrLDlrTvDlCzx+vgCFtSI=
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 
h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
@@ -192,8 +192,8 @@
 golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod 
h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod 
h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.6.0/go.mod 
h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
-golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
-golang.org/x/crypto v0.49.0/go.mod 
h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
+golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
+golang.org/x/crypto v0.52.0/go.mod 
h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod 
h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod 
h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod 
h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -201,8 +201,8 @@
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod 
h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
-golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
+golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
+golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
 golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
 golang.org/x/oauth2 v0.36.0/go.mod 
h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod 
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -214,20 +214,20 @@
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
-golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
+golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod 
h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod 
h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
-golang.org/x/term v0.41.0/go.mod 
h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
+golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
+golang.org/x/term v0.43.0/go.mod 
h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
-golang.org/x/text v0.36.0/go.mod 
h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
+golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
+golang.org/x/text v0.38.0/go.mod 
h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
 golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
 golang.org/x/time v0.15.0/go.mod 
h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod 
h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -249,12 +249,12 @@
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod 
h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80=
-k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34=
-k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ=
-k8s.io/apimachinery v0.36.0/go.mod 
h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc=
-k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c=
-k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y=
+k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY=
+k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg=
+k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ=
+k8s.io/apimachinery v0.36.2/go.mod 
h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4=
+k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI=
+k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0=
 k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
 k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
 k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a 
h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/k0sctl-0.30.1/phase/configure_k0s_test.go 
new/k0sctl-0.31.0/phase/configure_k0s_test.go
--- old/k0sctl-0.30.1/phase/configure_k0s_test.go       2026-05-08 
13:16:19.000000000 +0200
+++ new/k0sctl-0.31.0/phase/configure_k0s_test.go       2026-06-18 
13:20:25.000000000 +0200
@@ -67,7 +67,7 @@
 
        config, err := p.configFor(h)
        require.NoError(t, err)
-       require.Equal(t, "", apiAddressFromConfig(t, config))
+       require.Empty(t, apiAddressFromConfig(t, config))
 }
 
 type apiSpec struct {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/k0sctl-0.30.1/phase/gather_facts.go 
new/k0sctl-0.31.0/phase/gather_facts.go
--- old/k0sctl-0.30.1/phase/gather_facts.go     2026-05-08 13:16:19.000000000 
+0200
+++ new/k0sctl-0.31.0/phase/gather_facts.go     2026-06-18 13:20:25.000000000 
+0200
@@ -5,6 +5,7 @@
        "fmt"
        "strings"
 
+       "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1"
        
"github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster"
        "github.com/k0sproject/version"
        log "github.com/sirupsen/logrus"
@@ -16,6 +17,8 @@
 type GatherFacts struct {
        GenericPhase
        SkipMachineIDs bool
+
+       cplbVIPs map[string]struct{}
 }
 
 var (
@@ -30,6 +33,19 @@
        return "Gather host facts"
 }
 
+// Prepare the phase
+func (p *GatherFacts) Prepare(config *v1beta1.Cluster) error {
+       p.Config = config
+       // Precompute the set of control plane load balancing virtual IPs once 
so
+       // that investigateHost (which may run concurrently per host) can do a 
cheap
+       // lookup instead of re-parsing the k0s config for every host. A nil 
cplbVIPs
+       // map is safe to read from, so leave it unset when there is no spec.
+       if config.Spec != nil {
+               p.cplbVIPs = config.Spec.CPLBVIPs()
+       }
+       return nil
+}
+
 // Run the phase
 func (p *GatherFacts) Run(ctx context.Context) error {
        return p.parallelDo(ctx, p.Config.Spec.Hosts, p.investigateHost)
@@ -82,8 +98,12 @@
 
                if h.PrivateInterface != "" {
                        if addr, err := h.Configurer.PrivateAddress(h, 
h.PrivateInterface, h.Address()); err == nil {
-                               h.PrivateAddress = addr
-                               log.Infof("%s: discovered %s as private 
address", h, addr)
+                               if _, isVIP := p.cplbVIPs[addr]; isVIP {
+                                       log.Debugf("%s: skipping autodetected 
private address %s because it is a control plane load balancing virtual IP", h, 
addr)
+                               } else {
+                                       h.PrivateAddress = addr
+                                       log.Infof("%s: discovered %s as private 
address", h, addr)
+                               }
                        }
                }
        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/k0sctl-0.30.1/phase/gather_facts_test.go 
new/k0sctl-0.31.0/phase/gather_facts_test.go
--- old/k0sctl-0.30.1/phase/gather_facts_test.go        1970-01-01 
01:00:00.000000000 +0100
+++ new/k0sctl-0.31.0/phase/gather_facts_test.go        2026-06-18 
13:20:25.000000000 +0200
@@ -0,0 +1,132 @@
+package phase
+
+import (
+       "context"
+       "errors"
+       "testing"
+
+       "github.com/k0sproject/dig"
+       "github.com/k0sproject/k0sctl/configurer/linux"
+       "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1"
+       
"github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster"
+       rigOS "github.com/k0sproject/rig/os"
+       "github.com/k0sproject/version"
+       "github.com/stretchr/testify/require"
+)
+
+// linux.Ubuntu already embeds configurer.Linux (via linux.Debian) and 
satisfies
+// configurer.Configurer, so embedding it alone gives the mock all the no-op
+// configurer methods.
+type privateAddressMock struct {
+       linux.Ubuntu
+       addr string
+       err  error
+}
+
+func (m *privateAddressMock) PrivateAddress(_ rigOS.Host, _, _ string) 
(string, error) {
+       return m.addr, m.err
+}
+
+// makeCPLBConfig builds a dig.Mapping representing a k0s config with CPLB 
settings.
+func makeCPLBConfig(enabled bool, cplbType string, vrrpVIPs []string, 
virtualServerIPs []string) dig.Mapping {
+       vsEntries := make([]any, len(virtualServerIPs))
+       for i, ip := range virtualServerIPs {
+               vsEntries[i] = dig.Mapping{"ipAddress": ip}
+       }
+       vrrpEntries := make([]any, len(vrrpVIPs))
+       for i, ip := range vrrpVIPs {
+               vrrpEntries[i] = ip
+       }
+       return dig.Mapping{
+               "spec": dig.Mapping{
+                       "network": dig.Mapping{
+                               "controlPlaneLoadBalancing": dig.Mapping{
+                                       "enabled": enabled,
+                                       "type":    cplbType,
+                                       "keepalived": dig.Mapping{
+                                               "vrrpInstances":  
[]any{dig.Mapping{"virtualIPs": vrrpEntries}},
+                                               "virtualServers": vsEntries,
+                                       },
+                               },
+                       },
+               },
+       }
+}
+
+func TestInvestigateHostPrivateAddress(t *testing.T) {
+       const iface = "eth0"
+
+       makePhase := func(k0sConfig dig.Mapping) *GatherFacts {
+               config := &v1beta1.Cluster{
+                       Spec: &cluster.Spec{
+                               K0s: &cluster.K0s{
+                                       Version: version.MustParse("v1.33.0"),
+                                       Config:  k0sConfig,
+                               },
+                       },
+               }
+               p := &GatherFacts{SkipMachineIDs: true}
+               // Prepare() precomputes the CPLB VIP set the same way the 
manager would
+               // before Run/investigateHost run.
+               require.NoError(t, p.Prepare(config))
+               return p
+       }
+
+       makeHost := func(addr string, addrErr error) *cluster.Host {
+               return &cluster.Host{
+                       HostnameOverride: "test-host",
+                       PrivateInterface: iface,
+                       Metadata:         cluster.HostMetadata{Arch: "amd64"},
+                       Configurer:       &privateAddressMock{addr: addr, err: 
addrErr},
+               }
+       }
+
+       const cplbVIP = "10.0.0.1"
+       const normalIP = "192.168.1.5"
+       cplbCfg := makeCPLBConfig(true, "Keepalived", nil, []string{cplbVIP})
+
+       tests := []struct {
+               name        string
+               phase       *GatherFacts
+               host        *cluster.Host
+               wantPrivate string
+       }{
+               {
+                       name:        "CPLB IP returned: private address not 
set",
+                       phase:       makePhase(cplbCfg),
+                       host:        makeHost(cplbVIP, nil),
+                       wantPrivate: "",
+               },
+               {
+                       name:        "non-CPLB IP returned: private address 
set",
+                       phase:       makePhase(cplbCfg),
+                       host:        makeHost(normalIP, nil),
+                       wantPrivate: normalIP,
+               },
+               {
+                       name:        "configurer error: private address not 
set",
+                       phase:       makePhase(cplbCfg),
+                       host:        makeHost("", errors.New("lookup failed")),
+                       wantPrivate: "",
+               },
+               {
+                       name:        "nil k0s config: non-CPLB check skipped, 
address set",
+                       phase:       makePhase(nil),
+                       host:        makeHost(normalIP, nil),
+                       wantPrivate: normalIP,
+               },
+               {
+                       name:        "CPLB disabled: configured VIP not treated 
as CPLB, address set",
+                       phase:       makePhase(makeCPLBConfig(false, 
"Keepalived", nil, []string{cplbVIP})),
+                       host:        makeHost(cplbVIP, nil),
+                       wantPrivate: cplbVIP,
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       require.NoError(t, 
tc.phase.investigateHost(context.Background(), tc.host))
+                       require.Equal(t, tc.wantPrivate, tc.host.PrivateAddress)
+               })
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/k0sctl-0.30.1/phase/gather_k0s_facts.go 
new/k0sctl-0.31.0/phase/gather_k0s_facts.go
--- old/k0sctl-0.30.1/phase/gather_k0s_facts.go 2026-05-08 13:16:19.000000000 
+0200
+++ new/k0sctl-0.31.0/phase/gather_k0s_facts.go 2026-06-18 13:20:25.000000000 
+0200
@@ -401,7 +401,7 @@
                }
 
                if p.Config.Spec.K0s.DynamicConfig {
-                       h.InstallFlags.AddOrReplace("--enable-dynamic-config")
+                       
h.InstallFlags.AddOrReplace("--enable-dynamic-config=true")
                }
        }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/k0sctl-0.30.1/phase/get_kubeconfig.go 
new/k0sctl-0.31.0/phase/get_kubeconfig.go
--- old/k0sctl-0.30.1/phase/get_kubeconfig.go   2026-05-08 13:16:19.000000000 
+0200
+++ new/k0sctl-0.31.0/phase/get_kubeconfig.go   2026-06-18 13:20:25.000000000 
+0200
@@ -3,9 +3,11 @@
 import (
        "context"
        "fmt"
+
        
"github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster"
        "github.com/k0sproject/rig/exec"
        "k8s.io/client-go/tools/clientcmd"
+       "k8s.io/client-go/tools/clientcmd/api"
 
        log "github.com/sirupsen/logrus"
 )
@@ -72,28 +74,55 @@
 // kubeConfig reads in the raw kubeconfig and changes the given address
 // and cluster name into it
 func kubeConfig(raw string, name string, address, user string) (string, error) 
{
-       cfg, err := clientcmd.Load([]byte(raw))
+       config, err := clientcmd.Load([]byte(raw))
        if err != nil {
-               return "", err
+               return "", fmt.Errorf("parse kubeconfig: %w", err)
        }
 
-       cfg.Clusters[name] = cfg.Clusters["local"]
-       delete(cfg.Clusters, "local")
-       cfg.Clusters[name].Server = address
+       // Prefer the explicit current-context, but fall back to the sole 
context
+       // when current-context is empty or points to a missing entry, as a
+       // kubeconfig with a single context may legally omit it.
+       contextName := config.CurrentContext
+       sourceContext := config.Contexts[contextName]
+       if sourceContext == nil {
+               if len(config.Contexts) != 1 {
+                       if contextName == "" {
+                               return "", fmt.Errorf("no current-context set 
and config does not contain exactly one context to fall back to")
+                       }
+                       return "", fmt.Errorf("current context %s not found in 
config", contextName)
+               }
+               for ctxName, ctx := range config.Contexts {
+                       contextName, sourceContext = ctxName, ctx
+               }
+       }
 
-       cfg.Contexts[name] = cfg.Contexts["Default"]
-       delete(cfg.Contexts, "Default")
-       cfg.Contexts[name].Cluster = name
-       cfg.Contexts[name].AuthInfo = user
+       sourceCluster := config.Clusters[sourceContext.Cluster]
+       if sourceCluster == nil {
+               return "", fmt.Errorf("cluster %s referenced by context %s not 
found in config", sourceContext.Cluster, contextName)
+       }
+       sourceCluster.Server = address
 
-       cfg.CurrentContext = name
+       sourceAuthInfo := config.AuthInfos[sourceContext.AuthInfo]
+       if sourceAuthInfo == nil {
+               return "", fmt.Errorf("auth info %s referenced by context %s 
not found in config", sourceContext.AuthInfo, contextName)
+       }
 
-       cfg.AuthInfos[user] = cfg.AuthInfos["user"]
-       delete(cfg.AuthInfos, "user")
+       config.Clusters = map[string]*api.Cluster{
+               name: sourceCluster,
+       }
+       config.AuthInfos = map[string]*api.AuthInfo{
+               user: sourceAuthInfo,
+       }
+       sourceContext.Cluster = name
+       sourceContext.AuthInfo = user
+       config.Contexts = map[string]*api.Context{
+               name: sourceContext,
+       }
+       config.CurrentContext = name
 
-       out, err := clientcmd.Write(*cfg)
+       out, err := clientcmd.Write(*config)
        if err != nil {
-               return "", err
+               return "", fmt.Errorf("serialize kubeconfig: %w", err)
        }
 
        return string(out), nil
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/k0sctl-0.30.1/phase/get_kubeconfig_test.go 
new/k0sctl-0.31.0/phase/get_kubeconfig_test.go
--- old/k0sctl-0.30.1/phase/get_kubeconfig_test.go      2026-05-08 
13:16:19.000000000 +0200
+++ new/k0sctl-0.31.0/phase/get_kubeconfig_test.go      2026-06-18 
13:20:25.000000000 +0200
@@ -13,6 +13,10 @@
        "k8s.io/client-go/tools/clientcmd"
 )
 
+const ExpectedClusterAndContextName = "expected-context"
+const ExpectedUserName = "expected-user"
+const ExpectedClusterURL = "https://example.org";
+
 func fakeReader(h *cluster.Host) (string, error) {
        return strings.ReplaceAll(`apiVersion: v1
 clusters:
@@ -33,6 +37,18 @@
 `, "\t", "  "), nil
 }
 
+// requireKubeConfigEqual compares two kubeconfig YAML strings semantically by
+// parsing both, so the assertion is not coupled to clientcmd.Write formatting
+// or field ordering, which can change across client-go versions.
+func requireKubeConfigEqual(t *testing.T, expected, actual string) {
+       t.Helper()
+       expectedConfig, err := clientcmd.Load([]byte(expected))
+       require.NoError(t, err)
+       actualConfig, err := clientcmd.Load([]byte(actual))
+       require.NoError(t, err)
+       require.Equal(t, expectedConfig, actualConfig)
+}
+
 func TestGetKubeconfig(t *testing.T) {
        cfg := &v1beta1.Cluster{
                Metadata: &v1beta1.ClusterMetadata{
@@ -63,3 +79,255 @@
        require.NoError(t, err)
        require.Equal(t, "https://[abcd:efgh:ijkl:mnop]:6443";, 
conf.Clusters["k0s"].Server)
 }
+
+func TestConfigExtensionsRemain(t *testing.T) {
+       input := `apiVersion: v1
+clusters:
+- cluster:
+    server: https://localhost:6443
+  name: test-cluster
+contexts:
+- context:
+    cluster: test-cluster
+    user: test-user
+  name: test-context
+current-context: test-context
+extensions:
+- extension:
+    test: test
+  name: test
+kind: Config
+users:
+- name: test-user
+  user: {}
+`
+       expectedOutput := `apiVersion: v1
+clusters:
+- cluster:
+    server: https://example.org
+  name: expected-context
+contexts:
+- context:
+    cluster: expected-context
+    user: expected-user
+  name: expected-context
+current-context: expected-context
+extensions:
+- extension:
+    test: test
+  name: test
+kind: Config
+users:
+- name: expected-user
+  user: {}
+`
+       actualOutput, err := kubeConfig(input, ExpectedClusterAndContextName, 
ExpectedClusterURL, ExpectedUserName)
+
+       require.NoError(t, err)
+       requireKubeConfigEqual(t, expectedOutput, actualOutput)
+}
+
+func TestContextExtensionsRemain(t *testing.T) {
+       input := `apiVersion: v1
+clusters:
+- cluster:
+    server: https://localhost:6443
+  name: local-cluster
+contexts:
+- context:
+    cluster: local-cluster
+    extensions:
+    - extension:
+        test: test
+      name: test
+    user: user
+  name: Default
+current-context: Default
+kind: Config
+users:
+- name: user
+  user: {}
+`
+       expectedOutput := `apiVersion: v1
+clusters:
+- cluster:
+    server: https://example.org
+  name: expected-context
+contexts:
+- context:
+    cluster: expected-context
+    extensions:
+    - extension:
+        test: test
+      name: test
+    user: expected-user
+  name: expected-context
+current-context: expected-context
+kind: Config
+users:
+- name: expected-user
+  user: {}
+`
+       actualOutput, err := kubeConfig(input, ExpectedClusterAndContextName, 
ExpectedClusterURL, ExpectedUserName)
+
+       require.NoError(t, err)
+       requireKubeConfigEqual(t, expectedOutput, actualOutput)
+}
+
+func TestNonCurrentContextObjectsAreDropped(t *testing.T) {
+       input := `apiVersion: v1
+clusters:
+- cluster:
+    server: https://example.org
+  name: test-cluster
+- cluster:
+    server: https://example.org
+  name: test-cluster2
+contexts:
+- context:
+    cluster: test-cluster
+    user: test-user
+  name: test-context
+- context:
+    cluster: test-cluster2
+    user: test-user2
+  name: test-context2
+current-context: test-context
+kind: Config
+users:
+- name: test-user
+  user: {}
+- name: test-user2
+  user: {}
+`
+       expectedOutput := `apiVersion: v1
+clusters:
+- cluster:
+    server: https://example.org
+  name: expected-context
+contexts:
+- context:
+    cluster: expected-context
+    user: expected-user
+  name: expected-context
+current-context: expected-context
+kind: Config
+users:
+- name: expected-user
+  user: {}
+`
+       actualOutput, err := kubeConfig(input, ExpectedClusterAndContextName, 
ExpectedClusterURL, ExpectedUserName)
+
+       require.NoError(t, err)
+       requireKubeConfigEqual(t, expectedOutput, actualOutput)
+}
+
+func TestMissingCurrentContextFallsBackToSoleContext(t *testing.T) {
+       input := `apiVersion: v1
+clusters:
+- cluster:
+    server: https://localhost:6443
+  name: test-cluster
+contexts:
+- context:
+    cluster: test-cluster
+    user: test-user
+  name: test-context
+kind: Config
+users:
+- name: test-user
+  user: {}
+`
+       expectedOutput := `apiVersion: v1
+clusters:
+- cluster:
+    server: https://example.org
+  name: expected-context
+contexts:
+- context:
+    cluster: expected-context
+    user: expected-user
+  name: expected-context
+current-context: expected-context
+kind: Config
+users:
+- name: expected-user
+  user: {}
+`
+       actualOutput, err := kubeConfig(input, ExpectedClusterAndContextName, 
ExpectedClusterURL, ExpectedUserName)
+
+       require.NoError(t, err)
+       requireKubeConfigEqual(t, expectedOutput, actualOutput)
+}
+
+func TestMissingContext(t *testing.T) {
+       input := `apiVersion: v1
+current-context: test-context
+kind: Config
+`
+       _, err := kubeConfig(input, ExpectedClusterAndContextName, 
ExpectedClusterURL, ExpectedUserName)
+
+       require.EqualError(t, err, "current context test-context not found in 
config")
+}
+
+func TestEmptyCurrentContextWithMultipleContexts(t *testing.T) {
+       input := `apiVersion: v1
+clusters:
+- cluster:
+    server: https://example.org
+  name: test-cluster
+- cluster:
+    server: https://example.org
+  name: test-cluster2
+contexts:
+- context:
+    cluster: test-cluster
+    user: test-user
+  name: test-context
+- context:
+    cluster: test-cluster2
+    user: test-user2
+  name: test-context2
+kind: Config
+`
+       _, err := kubeConfig(input, ExpectedClusterAndContextName, 
ExpectedClusterURL, ExpectedUserName)
+
+       require.EqualError(t, err, "no current-context set and config does not 
contain exactly one context to fall back to")
+}
+
+func TestMissingAuthInfo(t *testing.T) {
+       input := `apiVersion: v1
+clusters:
+- cluster:
+    server: https://example.org
+  name: test-cluster
+contexts:
+- context:
+    cluster: test-cluster
+    user: test-user
+  name: test-context
+current-context: test-context
+kind: Config
+`
+       _, err := kubeConfig(input, ExpectedClusterAndContextName, 
ExpectedClusterURL, ExpectedUserName)
+
+       require.EqualError(t, err, "auth info test-user referenced by context 
test-context not found in config")
+}
+
+func TestMissingCluster(t *testing.T) {
+       input := `apiVersion: v1
+contexts:
+- context:
+    cluster: test-cluster
+    user: test-user
+  name: test-context
+current-context: test-context
+kind: Config
+users:
+- name: test-user
+  user: {}
+`
+       _, err := kubeConfig(input, ExpectedClusterAndContextName, 
ExpectedClusterURL, ExpectedUserName)
+
+       require.EqualError(t, err, "cluster test-cluster referenced by context 
test-context not found in config")
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/k0sctl-0.30.1/phase/validate_hosts_test.go 
new/k0sctl-0.31.0/phase/validate_hosts_test.go
--- old/k0sctl-0.30.1/phase/validate_hosts_test.go      2026-05-08 
13:16:19.000000000 +0200
+++ new/k0sctl-0.31.0/phase/validate_hosts_test.go      2026-06-18 
13:20:25.000000000 +0200
@@ -6,7 +6,6 @@
        "testing"
        "time"
 
-       cfg "github.com/k0sproject/k0sctl/configurer"
        "github.com/k0sproject/k0sctl/configurer/linux"
        "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1"
        
"github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster"
@@ -15,7 +14,6 @@
 )
 
 type mockconfigurer struct {
-       cfg.Linux
        linux.Ubuntu
        skew time.Duration
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/k0sctl-0.30.1/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/flags_test.go 
new/k0sctl-0.31.0/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/flags_test.go
--- 
old/k0sctl-0.30.1/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/flags_test.go   
    2026-05-08 13:16:19.000000000 +0200
+++ 
new/k0sctl-0.31.0/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/flags_test.go   
    2026-06-18 13:20:25.000000000 +0200
@@ -99,7 +99,7 @@
                flags := Flags{"--flag1=1", "--flag2"}
                result, err := flags.GetBoolean("--flag3")
                require.NoError(t, err)
-               require.Equal(t, result, false)
+               require.False(t, result)
        })
 }
 
@@ -120,7 +120,7 @@
        flags := Flags{"--flag1", "--flag2=foo", "--flag3=bar"}
        m := flags.Map()
        require.Len(t, m, 3)
-       require.Equal(t, "", m["--flag1"])
+       require.Empty(t, m["--flag1"])
        require.Equal(t, "foo", m["--flag2"])
        require.Equal(t, "bar", m["--flag3"])
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/k0sctl-0.30.1/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/spec.go 
new/k0sctl-0.31.0/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/spec.go
--- old/k0sctl-0.30.1/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/spec.go     
2026-05-08 13:16:19.000000000 +0200
+++ new/k0sctl-0.31.0/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/spec.go     
2026-06-18 13:20:25.000000000 +0200
@@ -2,6 +2,7 @@
 
 import (
        "fmt"
+       "net"
        "strings"
 
        "github.com/creasty/defaults"
@@ -118,6 +119,9 @@
                                Enabled    bool   `yaml:"enabled"`
                                Type       string `yaml:"type"`
                                Keepalived struct {
+                                       VRRPInstances []struct {
+                                               VirtualIPs []string 
`yaml:"virtualIPs"`
+                                       } `yaml:"vrrpInstances"`
                                        VirtualServers []struct {
                                                IPAddress string 
`yaml:"ipAddress"`
                                        } `yaml:"virtualServers"`
@@ -127,22 +131,78 @@
        } `yaml:"spec"`
 }
 
+// cplbConfig parses the keepalived control plane load balancing configuration
+// from the k0s config. The second return value is false if the config can't be
+// parsed or CPLB is not enabled with the Keepalived type.
+func (s *Spec) cplbConfig() (*k0sCPLBConfig, bool) {
+       if s.K0s == nil {
+               return nil, false
+       }
+
+       cfg, err := yaml.Marshal(s.K0s.Config)
+       if err != nil {
+               return nil, false
+       }
+
+       k0scfg := &k0sCPLBConfig{}
+       if err := yaml.Unmarshal(cfg, k0scfg); err != nil {
+               return nil, false
+       }
+
+       cplb := k0scfg.Spec.Network.ControlPlaneLoadBalancing
+       if !cplb.Enabled || cplb.Type != "Keepalived" {
+               return nil, false
+       }
+
+       return k0scfg, true
+}
+
+// CPLBVIPs returns the set of control plane load balancing virtual IPs
+// (keepalived virtualServers and vrrpInstances virtualIPs) declared in the k0s
+// config. VRRP virtual IPs are included both in their raw configured form and,
+// when configured in CIDR notation, as the bare IP address so that either form
+// matches. Returns an empty set if CPLB is not enabled or not using 
Keepalived.
+func (s *Spec) CPLBVIPs() map[string]struct{} {
+       vips := make(map[string]struct{})
+
+       k0scfg, ok := s.cplbConfig()
+       if !ok {
+               return vips
+       }
+
+       keepalived := k0scfg.Spec.Network.ControlPlaneLoadBalancing.Keepalived
+       for _, vs := range keepalived.VirtualServers {
+               if vs.IPAddress != "" {
+                       vips[vs.IPAddress] = struct{}{}
+               }
+       }
+       for _, instance := range keepalived.VRRPInstances {
+               for _, vipCIDR := range instance.VirtualIPs {
+                       if vipCIDR == "" {
+                               continue
+                       }
+                       // the value is not validated to be in CIDR notation, 
so keep the
+                       // raw form as well as the parsed bare IP to match 
either way
+                       vips[vipCIDR] = struct{}{}
+                       if vip, _, err := net.ParseCIDR(vipCIDR); err == nil {
+                               vips[vip.String()] = struct{}{}
+                       }
+               }
+       }
+
+       return vips
+}
+
 func (s *Spec) clusterExternalAddress() string {
        if s.K0s != nil {
                if a := s.K0s.Config.DigString("spec", "api", 
"externalAddress"); a != "" {
                        return a
                }
 
-               if cfg, err := yaml.Marshal(s.K0s.Config); err == nil {
-                       k0scfg := k0sCPLBConfig{}
-                       if err := yaml.Unmarshal(cfg, &k0scfg); err == nil {
-                               cplb := 
k0scfg.Spec.Network.ControlPlaneLoadBalancing
-                               if cplb.Enabled && cplb.Type == "Keepalived" {
-                                       for _, vs := range 
cplb.Keepalived.VirtualServers {
-                                               if addr := vs.IPAddress; addr 
!= "" {
-                                                       return addr
-                                               }
-                                       }
+               if k0scfg, ok := s.cplbConfig(); ok {
+                       for _, vs := range 
k0scfg.Spec.Network.ControlPlaneLoadBalancing.Keepalived.VirtualServers {
+                               if addr := vs.IPAddress; addr != "" {
+                                       return addr
                                }
                        }
                }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/k0sctl-0.30.1/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/spec_test.go 
new/k0sctl-0.31.0/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/spec_test.go
--- 
old/k0sctl-0.30.1/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/spec_test.go    
    2026-05-08 13:16:19.000000000 +0200
+++ 
new/k0sctl-0.31.0/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/spec_test.go    
    2026-06-18 13:20:25.000000000 +0200
@@ -80,3 +80,218 @@
                require.Equal(t, "https://192.168.0.10:6443";, spec.KubeAPIURL())
        })
 }
+
+// cplbSpec builds a Spec with a k0s config containing the given CPLB settings.
+func cplbSpec(enabled bool, cplbType string, vrrpVIPs []string, 
virtualServerIPs []string) *Spec {
+       vsEntries := make([]any, len(virtualServerIPs))
+       for i, ip := range virtualServerIPs {
+               vsEntries[i] = dig.Mapping{"ipAddress": ip}
+       }
+       vrrpEntries := make([]any, len(vrrpVIPs))
+       for i, ip := range vrrpVIPs {
+               vrrpEntries[i] = ip
+       }
+       return &Spec{
+               K0s: &K0s{
+                       Config: dig.Mapping{
+                               "spec": dig.Mapping{
+                                       "network": dig.Mapping{
+                                               "controlPlaneLoadBalancing": 
dig.Mapping{
+                                                       "enabled": enabled,
+                                                       "type":    cplbType,
+                                                       "keepalived": 
dig.Mapping{
+                                                               
"vrrpInstances":  []any{dig.Mapping{"virtualIPs": vrrpEntries}},
+                                                               
"virtualServers": vsEntries,
+                                                       },
+                                               },
+                                       },
+                               },
+                       },
+               },
+       }
+}
+
+func TestCPLBVIPs(t *testing.T) {
+       tests := []struct {
+               name string
+               ip   string
+               spec *Spec
+               want bool // whether ip is reported as a CPLB VIP
+       }{
+               // nil / empty configs
+               {
+                       name: "nil k0s",
+                       ip:   "10.0.0.1",
+                       spec: &Spec{},
+                       want: false,
+               },
+               {
+                       name: "nil config",
+                       ip:   "10.0.0.1",
+                       spec: &Spec{K0s: &K0s{}},
+                       want: false,
+               },
+               {
+                       name: "empty config",
+                       ip:   "10.0.0.1",
+                       spec: &Spec{K0s: &K0s{Config: dig.Mapping{}}},
+                       want: false,
+               },
+               {
+                       name: "invalid IP string",
+                       ip:   "not-an-ip",
+                       spec: cplbSpec(true, "Keepalived", nil, 
[]string{"10.0.0.1"}),
+                       want: false,
+               },
+               {
+                       name: "empty IP string",
+                       ip:   "",
+                       spec: cplbSpec(true, "Keepalived", nil, 
[]string{"10.0.0.1"}),
+                       want: false,
+               },
+
+               // CPLB disabled
+               {
+                       name: "CPLB disabled, IP matches virtual server",
+                       ip:   "10.0.0.1",
+                       spec: cplbSpec(false, "Keepalived", nil, 
[]string{"10.0.0.1"}),
+                       want: false,
+               },
+               {
+                       name: "CPLB disabled, IP matches VRRP plain VIP",
+                       ip:   "10.0.0.1",
+                       spec: cplbSpec(false, "Keepalived", 
[]string{"10.0.0.1"}, nil),
+                       want: false,
+               },
+               {
+                       name: "CPLB disabled, IP matches VRRP CIDR",
+                       ip:   "10.0.0.1",
+                       spec: cplbSpec(false, "Keepalived", 
[]string{"10.0.0.1/24"}, nil),
+                       want: false,
+               },
+
+               // CPLB enabled but wrong type
+               {
+                       name: "CPLB enabled, type not Keepalived",
+                       ip:   "10.0.0.1",
+                       spec: cplbSpec(true, "Other", nil, 
[]string{"10.0.0.1"}),
+                       want: false,
+               },
+
+               // Virtual server IPs
+               {
+                       name: "first virtual server IP matches",
+                       ip:   "192.168.1.10",
+                       spec: cplbSpec(true, "Keepalived", nil, 
[]string{"192.168.1.10", "192.168.1.11"}),
+                       want: true,
+               },
+               {
+                       name: "non-first virtual server IP matches",
+                       ip:   "192.168.1.11",
+                       spec: cplbSpec(true, "Keepalived", nil, 
[]string{"192.168.1.10", "192.168.1.11"}),
+                       want: true,
+               },
+               {
+                       name: "virtual server IP no match",
+                       ip:   "192.168.1.99",
+                       spec: cplbSpec(true, "Keepalived", nil, 
[]string{"192.168.1.10", "192.168.1.11"}),
+                       want: false,
+               },
+
+               // VRRP VIPs as plain IPs (direct string match)
+               {
+                       name: "first VRRP plain IP matches",
+                       ip:   "10.0.0.1",
+                       spec: cplbSpec(true, "Keepalived", []string{"10.0.0.1", 
"10.0.0.2"}, nil),
+                       want: true,
+               },
+               {
+                       name: "non-first VRRP plain IP matches",
+                       ip:   "10.0.0.2",
+                       spec: cplbSpec(true, "Keepalived", []string{"10.0.0.1", 
"10.0.0.2"}, nil),
+                       want: true,
+               },
+               {
+                       name: "VRRP plain IP no match",
+                       ip:   "10.0.0.99",
+                       spec: cplbSpec(true, "Keepalived", []string{"10.0.0.1", 
"10.0.0.2"}, nil),
+                       want: false,
+               },
+
+               // VRRP VIPs as CIDRs (bare host address extracted)
+               {
+                       name: "first VRRP CIDR host IP matches",
+                       ip:   "10.0.0.1",
+                       spec: cplbSpec(true, "Keepalived", 
[]string{"10.0.0.1/24", "10.0.0.2/24"}, nil),
+                       want: true,
+               },
+               {
+                       name: "non-first VRRP CIDR host IP matches",
+                       ip:   "10.0.0.2",
+                       spec: cplbSpec(true, "Keepalived", 
[]string{"10.0.0.1/24", "10.0.0.2/24"}, nil),
+                       want: true,
+               },
+               {
+                       name: "VRRP CIDR host IP no match",
+                       ip:   "10.0.0.99",
+                       spec: cplbSpec(true, "Keepalived", 
[]string{"10.0.0.1/24", "10.0.0.2/24"}, nil),
+                       want: false,
+               },
+               {
+                       name: "VRRP CIDR network address does not match sibling 
host",
+                       ip:   "10.0.0.2",
+                       spec: cplbSpec(true, "Keepalived", 
[]string{"10.0.0.1/24"}, nil),
+                       want: false,
+               },
+
+               // Invalid CIDR entries in VRRP list (should be skipped)
+               {
+                       name: "invalid CIDR skipped, subsequent valid CIDR 
matches",
+                       ip:   "10.0.0.2",
+                       spec: cplbSpec(true, "Keepalived", []string{"bad-cidr", 
"10.0.0.2/24"}, nil),
+                       want: true,
+               },
+               {
+                       name: "invalid CIDR only, no match",
+                       ip:   "10.0.0.1",
+                       spec: cplbSpec(true, "Keepalived", 
[]string{"bad-cidr"}, nil),
+                       want: false,
+               },
+
+               // No CPLB entries at all
+               {
+                       name: "CPLB enabled Keepalived, no VRRPs and no virtual 
servers",
+                       ip:   "10.0.0.1",
+                       spec: cplbSpec(true, "Keepalived", nil, nil),
+                       want: false,
+               },
+
+               // Mixed: both VRRP and virtual servers present
+               {
+                       name: "IP matches virtual server among mixed entries",
+                       ip:   "172.16.0.5",
+                       spec: cplbSpec(true, "Keepalived", 
[]string{"10.0.0.1/24"}, []string{"172.16.0.5"}),
+                       want: true,
+               },
+               {
+                       name: "IP matches VRRP among mixed entries",
+                       ip:   "10.0.0.1",
+                       spec: cplbSpec(true, "Keepalived", 
[]string{"10.0.0.1/24"}, []string{"172.16.0.5"}),
+                       want: true,
+               },
+               {
+                       name: "IP matches neither in mixed entries",
+                       ip:   "192.168.0.1",
+                       spec: cplbSpec(true, "Keepalived", 
[]string{"10.0.0.1/24"}, []string{"172.16.0.5"}),
+                       want: false,
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       vips := tc.spec.CPLBVIPs()
+                       _, got := vips[tc.ip]
+                       require.Equal(t, tc.want, got)
+               })
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/k0sctl-0.30.1/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/uploadfile_test.go
 
new/k0sctl-0.31.0/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/uploadfile_test.go
--- 
old/k0sctl-0.30.1/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/uploadfile_test.go
  2026-05-08 13:16:19.000000000 +0200
+++ 
new/k0sctl-0.31.0/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/uploadfile_test.go
  2026-06-18 13:20:25.000000000 +0200
@@ -140,8 +140,8 @@
        u := &UploadFile{Source: "https://example.com/assets/app.tar.gz";, 
DestinationDir: "/opt"}
        require.NoError(t, u.ResolveRelativeTo(""))
        require.Equal(t, "/opt/app.tar.gz", u.DestinationFile)
-       require.Equal(t, "", u.Base)
-       require.Len(t, u.Sources, 0)
+       require.Empty(t, u.Base)
+       require.Empty(t, u.Sources)
 }
 
 func TestUploadFileResolveRelativeSingleFile(t *testing.T) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/k0sctl-0.30.1/pkg/retry/retry_test.go 
new/k0sctl-0.31.0/pkg/retry/retry_test.go
--- old/k0sctl-0.30.1/pkg/retry/retry_test.go   2026-05-08 13:16:19.000000000 
+0200
+++ new/k0sctl-0.31.0/pkg/retry/retry_test.go   2026-06-18 13:20:25.000000000 
+0200
@@ -87,7 +87,7 @@
                })
                elapsed := time.Since(start)
 
-               assert.Error(t, err)
+               assert.Error(t, err) //nolint:testifylint
                assert.Less(t, elapsed, 50*time.Millisecond)
        })
 
@@ -99,7 +99,7 @@
                })
                elapsed := time.Since(start)
 
-               assert.Error(t, err)
+               assert.Error(t, err) //nolint:testifylint
                assert.GreaterOrEqual(t, elapsed.Milliseconds(), int64(10))
        })
 
@@ -136,7 +136,7 @@
                        tries++
                        return errors.New("some error")
                })
-               assert.Error(t, err, "foo")
+               assert.Error(t, err, "foo") //nolint:testifylint
                assert.Equal(t, 2, tries)
        })
 
@@ -146,7 +146,7 @@
                        tries++
                        return errors.Join(ErrAbort, errors.New("some error"))
                })
-               assert.Error(t, err, "foo")
+               assert.Error(t, err, "foo") //nolint:testifylint
                assert.Equal(t, 1, tries)
        })
 }
@@ -165,6 +165,6 @@
        })
        elapsed := time.Since(start)
 
-       assert.Error(t, err)
+       assert.Error(t, err) //nolint:testifylint
        assert.GreaterOrEqual(t, elapsed.Milliseconds(), int64(5))
 }

++++++ k0sctl.obsinfo ++++++
--- /var/tmp/diff_new_pack.naQi17/_old  2026-06-18 18:45:35.721489832 +0200
+++ /var/tmp/diff_new_pack.naQi17/_new  2026-06-18 18:45:35.749491000 +0200
@@ -1,5 +1,5 @@
 name: k0sctl
-version: 0.30.1
-mtime: 1778238979
-commit: 4afdb2b1bf3f10f57beddf00a85b72a0a0000927
+version: 0.31.0
+mtime: 1781781625
+commit: def1a095778cd5ca0ef703d9b6fe13789b17f67a
 

++++++ vendor.tar.gz ++++++
++++ 47489 lines of diff (skipped)

Reply via email to