Skip to content

v1.1.0 — port-forward + logs UX

Choose a tag to compare

@LywwKkA-aD LywwKkA-aD released this 11 May 21:38
· 2 commits to master since this release

Two themed improvements on top of v1.0.0 — plus the polish commit that
made the port-forward feature actually usable on first try.

Port-forward with persisted intents

You can now expose any Service, Pod or Deployment on a local port from
inside k4s — no need to drop to a shell for kubectl port-forward.

  • Press f on a row in services / pods / deployments.
  • A centred popup asks for local:remote. The local default is
    smart: ports < 1024 get bumped by 8000 (80 → 8080, 443 → 8443) so
    non-root users can bind them without "permission denied". Editable
    — change either side before Enter.
  • The forward starts immediately and you land on the new forwards
    view to confirm it is running.

The forwards view

Open it any time with :pf (or :forwards).

Key Action
Enter start a stopped forward
s stop a running forward
r restart
d delete from state

Persistence — the "intent" model

The state file at `$XDG_STATE_HOME/k4s/portforwards.json`
(default `~/.local/state/k4s/`) remembers every forward you have ever
declared
. Quitting k4s ends the live tunnels (they are SPDY connections
inside this process), but on the next launch k4s shows a centred popup:

Found N saved forwards from a previous session.
y / Enter revive all · n / Esc skip · :pf to manage later

Atomic write (temp + rename), XDG_STATE_HOME honoured, missing file
treated as empty state.

Robust error handling

  • A local-port pre-check (`net.Listen` + immediate close) runs before
    anything is persisted. Privileged or busy ports surface as friendly
    messages right in the command-bar — "permission denied binding local
    port 80 (ports < 1024 need root — try 8080)"
    , "local port 8080
    already in use — pick another"
    . No half-dead entries pile up in the
    forwards list.
  • kubectl's stderr output (`Unable to listen on port X: bind: …`) is
    captured into a buffer instead of leaking to the real terminal and
    shredding the TUI render. Stdout (info chatter like `Forwarding from
    127.0.0.1:8080 -> 80`) is captured separately and not treated as
    an error — the supervisor only flips status to red on real stderr
    diagnostics.
  • Manager.supervise drains pending errors after Done so the
    fw.ForwardPorts race (it both closes Done and writes to Err) never
    loses the real cause.

Architecture (for the curious)

  • `internal/k8s/portforward.go` — `Client.StartPodPortForward` builds
    an SPDY dialer from the retained `rest.Config` and drives a
    `portforward.PortForwarder` in a goroutine. Service and Deployment
    targets resolve to a backing pod via the label selector +
    first-ready pod, matching kubectl's behaviour. Session lifetime is
    bound to StopCh only, not the caller's ctx.
  • `internal/forwards/state.go` — JSON wire format with `Validate()`,
    pure `Upsert`/`Remove` returning new States.
  • `internal/forwards/manager.go` — owns the state and active map,
    exposes a one-shot `Changes()` channel for the UI to subscribe to,
    and supervises sessions so status flips to error or stopped
    without manual refreshes. `List()` returns a stable sort
    `(namespace, kind, name, localport)` so the UI cursor doesn't jump.
  • `internal/tui/views/forwards/` — new list view subscribed to
    `Manager.Changes`.

Logs UX

Three quality-of-life fixes for the logs view, motivated by
`--tail=5000` making the UI feel "stuck" while it scrolled to the
bottom.

  1. Drain batching. The old loop scheduled one Update tick per log
    line, each of which re-formatted every line with lipgloss
    (O(N²) on the initial backlog). Now one tick reads everything the
    channel has ready (cap 1000) and rebuilds the viewport once.
  2. Incremental render cache. Formatted lines are kept in a parallel
    slice so appends are O(1). Mode toggles that need a full reformat
    (`t`, `w`) rebuild explicitly.
  3. Soft wrap + horizontal scroll. Long lines used to be silently
    clipped. Now:
    • `w` toggles wrap mode (soft-wrap to viewport width).
    • `←` / `→` pan horizontally in no-wrap mode, 10 columns at a
      time. Status row shows `[→ N cols]` while panned.
    • The keys go inert in wrap mode and drop from the help footer.

Quality

  • `go vet` ✓
  • `golangci-lint` ✓ (0 issues)
  • race detector clean
  • new test packages: `internal/forwards` (~75% cov), expanded coverage
    in `internal/tui/views/logs` and `internal/k8s`.

Install

```bash
go install github.com/LywwKkA-aD/k4s/cmd/k4s@v1.1.0
```

Or via source: `git clone https://github.com/LywwKkA-aD/k4s && cd k4s && make demo && make run`.

Website: https://k4scli.io