A local Kubernetes control-plane client with a Rust workspace foundation: a protocol crate shared by native and web frontends, a fake-backed backend kernel, and an embeddable Axum control server. The release candidate includes real kubeconfig-backed reads and operations, guarded YAML, logs, external desktop shells, bounded recovery, browser/native frontends, load budgets, and native/server packages.
Normal desktop and standalone launches use the real Kubernetes adapter and
standard kubeconfig discovery. Deterministic fake mode remains available only
through the explicit standalone --fake development/test flag; a missing or
invalid kubeconfig never silently changes backend.
Pre-built binaries and desktop packages are available on the GitHub Releases page for macOS, Linux, and Windows.
Download k10s_<version>_aarch64.dmg from the Releases page, open it, and drag k10s.app into /Applications.
Note
macOS Gatekeeper ("k10s.app is damaged and can't be opened")
Because k10s is community-distributed without a commercial Apple Developer notarization profile, macOS Gatekeeper quarantines the downloaded bundle and may report the application as damaged.
To resolve this, run the following in Terminal after installing:
xattr -cr /Applications/k10s.app
codesign --force --deep --sign - /Applications/k10s.appDesktop builds are distributed as .deb, .AppImage, and .tar.gz packages.
Desktop installers are distributed as .msi and .exe (NSIS) installers.
| Path | Purpose |
|---|---|
crates/k10s-protocol |
Wire protocol: frames, envelopes, error contract (no platform dependencies) |
crates/k10s-backend |
Backend port, kernel, and the deterministic fake adapter |
crates/k10s-server |
Embeddable Axum control server: WebSocket control socket, auth, outbound scheduler, readiness probes, ordered shutdown |
crates/k10s-ui |
Shared client state machine and transport used by both frontends |
apps/k10s-desktop |
Native egui/eframe app embedding the server on a random loopback port |
apps/k10s-web |
WASM frontend built with Trunk |
cargo test --locked --workspace # all unit + integration tests
cargo run -p k10s-desktop # desktop app (embeds the server)
cargo run -p k10s-server-app # standalone server on 127.0.0.1:8080Open shell is available only in the native desktop application while it is
connected to the embedded server that the same application started. It opens
one independent kubectl exec -it session in the system terminal; Finback does
not embed, monitor, or terminate that terminal. Web builds and desktop clients
connected to a standalone or remote server have no Shell tab, shortcut,
command-palette action, placeholder, or external-shell button.
The local machine must provide kubectl, a usable kubeconfig, and a terminal
launcher. macOS opens a private executable .command file with open; Linux
tries xdg-terminal-exec, x-terminal-emulator, gnome-terminal, konsole,
then kitty; Windows starts a BOM/CRLF PowerShell script with
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File and a new
console. See configuration,
security, and troubleshooting
for descriptor reproduction, Pod-identity, and cleanup details.
Standalone server environment:
K10S_BIND_ADDR— listener address (default127.0.0.1:8080)K10S_DIST_DIR— optional external Trunk output tree for development. Release binaries embed the exact fingerprinteddist/built before Cargo runs.K10S_ACCESS_TOKEN_FILE— path of a file containing the access token (surrounding whitespace trimmed). When set, it always takes precedence overK10S_ACCESS_TOKEN. An empty or unreadable file refuses startup.K10S_ACCESS_TOKEN— inline access token. Required for non-loopback binds; loopback defaults to an empty token.
The server logs structured, credential-free telemetry to stderr at info
level. SIGINT/SIGTERM trigger the ordered drain described below.
Operator references: configuration, deployment, security, troubleshooting, and protocol.
Access tokens. The token travels exclusively in the first protocol Hello
it is accepted on, and it must never reach URLs, built assets,
localStorage-style persistence, logs, or error payloads:
- Server config types redact the token from every
Debugrendering. - The web gate holds the entered value only in an ephemeral form buffer and hands it straight to the protocol client as connection state; on successful authentication the buffer is discarded. Persisted settings carry only the credential-free endpoint URL, and the transport rejects any WebSocket URL containing userinfo, query strings, or fragments.
- Secret sources resolve with documented precedence:
K10S_ACCESS_TOKEN_FILEwins overK10S_ACCESS_TOKEN; empty files refuse to start; no source at all is only valid for loopback-only development binds (StandaloneConfigstill rejects non-loopback listeners without an explicit token). - Token comparison inside the control socket uses a constant-time byte compare, and connections are bounded before authentication completes (unauthenticated connection cap, first-frame deadline, frame/message size limits).
Same-origin enforcement. Control-socket upgrades that carry a browser
Origin header must match the request's own Host authority (default ports
80/443 compare equal to their implicit form); anything else is rejected with
HTTP 403. Native desktop clients send no Origin and are unconstrained, so
they keep working unchanged. Browsers enforce no reliable same-origin policy
on WebSocket connections, so this server-side check is the enforcement point,
not defense in depth; clients must not rely on browser behavior for it.
Reverse proxies / trusted headers. k10s never infers client identity or
trust from request headers: X-Forwarded-*, X-Real-Ip, and similar are
ignored by default. When placing the server behind a TLS/auth-terminating
reverse proxy, enforce authentication there and keep the host/origin seen by
k10s identical to what the browser uses (same host end-to-end); otherwise the
same-origin check will refuse upgrades.
trunk build --release # builds apps/k10s-web into dist/
cargo check --locked -p k10s-web --target wasm32-unknown-unknown
npx playwright install chromium # once, for the browser smoke
npx playwright test tests/browser/foundation.spec.ts --project=chromiumTrunk.toml pins locked = true; CI pins Trunk 0.21.14. The web entry derives
only the scheme and authority from window.location and replaces the path with
the root-level control endpoint; it never forces WSS on HTTP development pages.
The connected UI prototype closes with a quality gate covering every loading/empty/stale/error state of the approved screen set:
crates/k10s-ui/tests/ui_resilience.rs— loading vs empty vs filtered-empty lists, stale-connection banners, textual (never color-only) status and metrics conditions, conflict reasons inside operation dialogs, a gone-resource projection for deleted selections, unavailable GVKs after context switches, disconnected logs that retain history, external shells that never create navigation guards, keyboard focus order, and minimum-size non-overlap.crates/k10s-ui/tests/ui_snapshots.rs+tests/snapshots/*.txt— stable accessibility-tree snapshots of the approved screens. These are deterministic text dumps (roles, labels, values in widget order) rather than pixel PNGs: byte-stable across renderers and CI runners, and they double as the AccessKit coverage. Regenerate intentionally withK10S_UPDATE_SNAPSHOTS=1 cargo test -p k10s-ui --test ui_snapshots.
A deterministic 50,000-object / 1,000-node fake dataset
(FakeKubernetes::with_capacity) anchors the capacity gate; the same fixed
distribution is used end to end:
| Command | Proves |
|---|---|
cargo bench -p k10s-backend --bench fake_scale -- --test |
dataset build time, full pod-list query time, subscription snapshot registration, and stable live memory across repeated queries |
cargo test -p k10s-server --test fake_capacity |
the whole dominant-kind snapshot (~18,750 rows) streams through the real control socket as bounded ≤16-row pages and reassembles completely |
tests/kind/cluster.sh up then cargo test --locked -p k10s-backend --test kind_read_path -- --ignored --nocapture |
real kubeconfig contexts, discovery, built-ins, CRDs, lists/details/YAML, owner traversal, events, RBAC denial, honest missing metrics, and live watch apply/delete recovery against ephemeral kind |
cargo test --locked -p k10s-backend --test kind_operations -- --ignored --nocapture (with the same kind cluster) |
least-privilege server-side dry-run/apply, UID/RV conflicts, scale/restart/delete propagation, Job/CronJob actions, bounded logs, RBAC denial, and reconciliation after an induced lost mutation response |
cargo test --locked -p k10s-server --test kind_server_read_path -- --ignored --nocapture |
the same configured cluster reaches clients through the authenticated real control WebSocket and never falls back to fake fixture contexts |
cargo bench -p k10s-ui --bench ui_capacity -- --test |
the shell renders/filters/scrolls the 50k-object model at a fixed 1440×900 viewport within recorded frame-time and allocation-per-frame ceilings |
Recorded baselines (developer workstation; CI ceilings carry an order of magnitude of headroom while still catching order-of-magnitude regressions):
- backend: dataset build ≈ 0.11 s, full pod-list query ≈ 11 ms, snapshot registration ≈ 10 ms, no live-memory drift across repeated queries
- server: full socket transfer of ~1,175 bounded frames ≈ 1.4 s (debug build)
- UI: ≈ 1.3 ms average frame time with virtualized rows (~30k allocations per frame), ceilings 100 ms / 150k — losing row virtualization or doing model-sized work per frame breaches them
Both benches are hand-rolled (harness = false) because the ceilings
themselves are the assertions; they run once under --test for CI
determinism. Plan 5 repeats this gate against real runtime pressure.
| Probe | Semantics |
|---|---|
/healthz |
Liveness: 200 ok\n while the process event loop is alive — including during shutdown — until the listener itself closes |
/readyz |
Readiness: 503 starting\n during initialization, 200 ready\n only after initialization and request acceptance, 503 initialization failed\n after a failed startup, 503 draining\n once shutdown begins |
Probe bodies are fixed strings: no kubeconfig paths or credentials.
Shutdown is an explicitly sequenced state machine; every stage is published as
a k10s_server::lifecycle log event:
- Mark not-ready —
/readyzflips to503 draining. - Stop accepting application connections — new control upgrades are refused.
- Send
ShutdownNoticeto connected sessions and close the mutation gate; status reads keep working for a bounded grace window (drain_grace_timeout). - Cancel watches and log streams as each socket task unwinds.
- Drain tracked connection tasks under one absolute hard deadline
(
drain_timeout); survivors are force-closed, any task that still ignores the force signal is aborted and joined before returning, andshutdownreportsTimedOut. Upgrades accepted but not yet running hold a pending registration so they cannot slip past the drain. - Close the listener last — forced teardown completes inside the serving
lifetime, so
/healthzstays reachable until the listener itself closes.
Connection tasks are tracked with tokio_util::TaskTracker; in-flight requests
observe cancellation and return structured errors. Access tokens are sent only
in the first Hello frame and are never logged, persisted, or embedded in
error payloads or probe bodies.
.github/workflows/ci.yml runs, per pull request and on main:
- Unit tests —
cargo fmt --all -- --check, Clippy with-D warnings, andcargo test --locked --workspace --all-targets - WASM check —
cargo check --locked -p k10s-web --target wasm32-unknown-unknown - Web foundation — pinned Trunk 0.21.14 release build plus the Chromium Playwright smoke against the standalone server
- External shell platforms — an explicit Linux/macOS/Windows matrix runs generated scripts against fake kubectl and exercises native secure-storage and cleanup primitives without opening a graphical terminal
- Native platform smoke — release-mode server/desktop builds and loopback launch probes on Windows and macOS hosted runners
Release builds are automated by .github/workflows/release.yml
on hosted runners. The fixed build order is Trunk 0.21.14, the locked release
workspace, cargo-packager 0.11.8, cargo-dist 0.32.0, then the OCI image.
Desktop outputs are .deb + .AppImage, .msi + NSIS .exe, and
.app + .dmg; the standalone server is a per-target .tar.xz/.zip with
the same web bundle embedded, plus a non-root OCI image.
Every push to main cuts a patch release automatically: the workflow bumps the
matching version in Cargo.toml, Packager.toml, and Cargo.lock, lands
the change as chore(release): vX.Y.Z through a release PR that is merged
automatically (main requires changes to come through pull requests), tags the
merged commit vX.Y.Z, and re-dispatches the Release workflow at that tag
(GitHub suppresses workflow runs triggered by GITHUB_TOKEN pushes, so the tag
push cannot start the packaging phase by itself). The packaging run builds
every platform and publishes the GitHub release with generated release notes.
The merge-commit-based flow keeps main's tip free of a chore(release):
subject, so every push cuts exactly one release and the pipeline cannot
re-trigger itself.
To cut a release manually — e.g. a minor or major bump — run the Release
workflow from the Actions tab with bump set to patch, minor, or major
on branch main. The first run lands the bump via an auto-merged release PR,
tags the merged commit, and re-dispatches the workflow at the new tag; the
packaging run builds and publishes.
Local release verification uses the same order:
trunk build --release
cargo build --locked --release --workspace
cargo install cargo-packager --version 0.11.8 --locked
cargo packager --release
cargo install cargo-dist --version 0.32.0 --locked
dist build --artifacts=local
docker buildx build --load -f packaging/container/Dockerfile -t k10s:test .Signing remains opt-in and secrets are never stored in the repository. Windows signing supplies the certificate to the runner and configures the packager thumbprint/timestamp inputs at release time. macOS signing/notarization supplies an Apple signing identity, App Store Connect issuer/key ID, and private key via the runner keychain/environment. Pull requests exercise source, web, native build, and loopback launch gates; installer/archive creation and OCI packaging run only for a release tag or an explicit manual release-pipeline smoke test.
A manual workflow_dispatch run with bump unset (or none) builds all
platform artifacts without publishing. Run it before releasing whenever
packaging metadata, the container definition, release tooling, or release
workflow changes.