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)
