Compare commits

...
Author SHA1 Message Date
lukaszraczylo 3471502b7a docs: restore the comparison, advanced features and install sections
The README now matches the landing page. It restores the comparison with
sources, retry strategy, kftray migration, signal handling, signature
verification and build from source.

It fixes four claims the code contradicts: the maxBodySize default, the
deployment resource type, the file watcher delay and the verbose table
status markers.
2026-10-02 03:17:39 +01:00
lukaszraczylo 17f9248588 docs(site): rebuild the landing page with comparison, install and reference sections
The minimal rewrite dropped content the page needs. The new page has a
hero with an install command, a terminal mock, the real screenshot,
feature rows with snippets from the repository, a dated comparison with
kubectl, k9s, kubefwd and kftray with source links, install tabs,
config and key references, troubleshooting and an FAQ.

Cells that a project does not document show as Not documented. The page
uses one stylesheet and no external scripts.
2026-10-02 03:17:38 +01:00
lukaszraczylo 2751b02428 docs: rewrite the README and update the feature list
The README now has a feature table, install steps, a flag table, a
config reference and troubleshooting. It documents tui.columns, typed
namespace entry, shell completion and the --context flag.

It drops the comparison table with other tools, which the repository
cannot back up, and corrects the supported resource types and the Go
version.
2026-10-02 02:49:13 +01:00
lukaszraczylo 7e08fd7758 docs(site): rewrite the product page in plain language
The page lists what kportal does with the flags, keys and config keys
that exist in the code. It drops the CDN assets, gradients and icon
grids, and keeps the light and dark themes.

It now covers tui.columns, typed namespace entry, completion, the log
format flag and the --context flag. The claim about a 100ms file watcher
is removed because the code has no such debounce.
2026-10-02 02:49:12 +01:00
2 changed files with 1511 additions and 1383 deletions
+336 -312
View File
@@ -8,51 +8,99 @@
<a href="https://goreportcard.com/report/github.com/lukaszraczylo/kportal"><img src="https://goreportcard.com/badge/github.com/lukaszraczylo/kportal" alt="Go Report Card"></a> <a href="https://goreportcard.com/report/github.com/lukaszraczylo/kportal"><img src="https://goreportcard.com/badge/github.com/lukaszraczylo/kportal" alt="Go Report Card"></a>
</p> </p>
<p align="center"> # kportal
<strong>Kubernetes port-forward manager with interactive terminal UI</strong>
</p>
kportal manages multiple Kubernetes port-forwards with an interactive terminal interface. It provides real-time status updates, automatic reconnection, hot-reload configuration, and mDNS hostname publishing. kportal runs many Kubernetes port-forwards from one YAML file and shows their status in a terminal UI. It reconnects dropped forwards, reloads the file when it changes, and can run without the UI.
![kportal Screenshot](docs/kportal-screenshot.png) ![kportal Screenshot](docs/kportal-screenshot.png)
## ✨ Features Screenshot from kportal v0.2.17. The current key bar also lists `PgUp`/`PgDn`, `b` (Bench) and `l` (Logs).
- **Interactive TUI** - Terminal interface with keyboard navigation Website: <https://lukaszraczylo.github.io/kportal>
- **Live management** - Add, edit, and delete port-forwards without restarting
- **Auto-reconnect** - Exponential backoff retry on connection failures
- **Hot-reload** - Configuration changes applied automatically
- **Health monitoring** - Multiple check methods with stale connection detection
- **Multi-context** - Support for multiple Kubernetes contexts and namespaces
- **Pod restart handling** - Automatic reconnection when pods restart
- **Label selectors** - Dynamic pod targeting using label selectors
- **Port conflict detection** - Validates port availability with PID information
- **mDNS hostnames** - Access forwards via `.local` hostnames
- **HTTP traffic logging** - Real-time HTTP request/response logging for debugging
- **Connection benchmarking** - Built-in HTTP benchmarking with latency statistics
- **Headless mode** - Background operation for scripting and automation
## 🔄 Comparison with Other Tools ## Contents
| Feature | kportal | [k9s](https://k9scli.io/) | [Kube Forwarder](https://kube-forwarder.pixelpoint.io/) | [kftray](https://kftray.app/) | - [Features](#features)
|---------|---------|------|----------------|--------| - [Comparison](#comparison-with-other-tools)
| **Interface** | Terminal TUI | Terminal TUI | Desktop GUI (Electron) | Desktop GUI + TUI | - [Installation](#installation)
| **Persistent Config** | ✅ YAML file | ❌ Session only | ✅ JSON bookmarks | ✅ JSON + Git sync | - [Quick start](#quick-start)
| **Auto-reconnect** | ✅ Exponential backoff | ❌ Manual | ✅ Basic | ✅ Watch API | - [Configuration](#configuration)
| **Hot-reload Config** | ✅ File watch + SIGHUP | ❌ | ❌ | ❌ | - [Usage](#usage)
| **Health Checks** | ✅ TCP + data-transfer | ❌ | ❌ | ❌ | - [Status indicators](#status-indicators)
| **Stale Connection Detection** | ✅ Age + idle tracking | ❌ | ❌ | ❌ | - [Advanced features](#advanced-features)
| **HTTP Traffic Logging** | ✅ Built-in viewer | ❌ | ❌ | ✅ | - [Migration from kftray](#migration-from-kftray)
| **Connection Benchmarking** | ✅ Built-in | ✅ Via Hey | ❌ | ❌ | - [Signal handling](#signal-handling)
| **mDNS Hostnames** | ✅ `.local` domains | ❌ | ❌ | ❌ | - [Troubleshooting](#troubleshooting)
| **Label Selectors** | ✅ | ✅ | ❌ | ✅ | - [Development](#development)
| **Multi-context** | ✅ | ✅ | ✅ | ✅ | - [Telemetry](#telemetry)
| **Headless Mode** | ✅ | ❌ | ❌ | ❌ | - [License](#license)
| **System Tray** | ❌ | ❌ | ❌ | ✅ |
| **UDP Support** | ❌ | ❌ | ❌ | ✅ Proxy relay |
| **Dependencies** | Single binary | Single binary | Electron | Tauri + kubectl |
## 📦 Installation ## Features
| Area | What kportal does |
|------|-------------------|
| Forwards | Forwards to `service/<name>`, `pod/<name>` (exact name or name prefix), or `pod` with a label `selector`. Only TCP is supported. |
| Terminal UI | Add (`n`), edit (`e`), delete (`d`) and toggle (`Space`) forwards without a restart. The add wizard writes the config file. |
| Table columns | `tui.columns` sets which columns the forwards table shows, their order and their width. It applies to the UI and to the `-v` table. |
| Add wizard | If the cluster does not allow listing namespaces, the wizard asks you to type the namespace. |
| Reconnect | Retries with exponential backoff: 1s, 2s, 4s, 8s, then 10s, with 10% jitter. It does not stop retrying. |
| Pod restarts | Forwards by pod prefix or selector connect to a new pod after a restart. |
| Health checks | `tcp-dial` or `data-transfer` check every 3s by default. A connection is stale when it is older than `maxConnectionAge` and idle, or idle longer than `maxIdleTime`. |
| Hot reload | The file watcher and `SIGHUP` reload the config. An invalid config is rejected and the previous one stays active. |
| Contexts | Several Kubernetes contexts and namespaces in one file. `--context` selects which contexts run. |
| Port conflicts | Startup and reload checks report a busy local port and the process that holds it. |
| mDNS | With `mdns.enabled: true`, each forward is reachable as `<alias>.local`. Without an alias, the resource name is used. |
| HTTP log | Per-forward request and response log in the UI, with detail view, filter, search, JSON formatting, gzip/deflate decoding and clipboard copy. |
| Header redaction | With `includeHeaders: true`, values of sensitive headers such as `Authorization` and `Cookie` become `[REDACTED]`. You cannot turn this off. |
| Benchmark | Press `b` to send HTTP requests through a forward. Reports success and failure counts, min/max/avg latency, P50/P95/P99, requests per second and status codes. |
| Generate | `kportal generate` lists services in a cluster and appends the ones you pick to the config with consecutive local ports. |
| Headless | `-headless` runs without the UI and logs to stderr. `-log-format json` switches the log format. |
| Import | `-convert` turns a kftray JSON file into a kportal YAML file. |
| Shell completion | `kportal completion` prints a bash, zsh or fish completion script, or installs it. |
| Verified installer | `install.sh` checks the archive SHA-256 and, if `cosign` is on `PATH`, the keyless signature of the checksums file. |
| Updates | `-update` checks for a newer release. `-version` prints the version. |
## Comparison with other tools
Compared on 2026-10-02 against each project's README and docs. Check their repositories for changes. The kportal column comes from this repository's code. "Not documented" means the project's README and docs do not say.
| Feature | kportal | kubectl port-forward | k9s | kubefwd | kftray |
|---------|---------|----------------------|-----|---------|--------|
| Many forwards in one config file | Yes | No | Partial (annotations configure forwards; no forward list file) | Partial (`example.fwdconf.yml` exists) | Yes |
| Automatic reconnect | Yes | Not documented | Not documented | Yes | Yes |
| Follows pod restarts by label or name prefix | Yes | Not documented | Not documented | Yes (label filter) | Yes |
| Terminal UI of all forwards | Yes | No | Yes (PortForward view, alias `pf`; forwards last for the session) | Yes | Yes |
| GUI | No | No | No | No | Yes |
| Config hot reload | Yes (file watcher, `SIGHUP`) | No | Partial (`benchmarks.yaml` only) | Not documented | Not documented |
| HTTP request and response logging | Yes | No | Not documented | Not documented | Yes |
| Health checks | Yes (`tcp-dial`, `data-transfer`) | Not documented | Not documented | Not documented | Partial (README says it tracks pod health) |
| mDNS or friendly hostnames | Yes (mDNS) | No | No | Partial (edits `/etc/hosts`, not mDNS; needs root) | Not documented |
| Built-in benchmark | Yes | No | Yes (`hey`, key `b`) | Not documented | Not documented |
| Imports another tool's config | Yes (kftray JSON) | No | No | Not documented | Partial (JSON, GitHub repos, service annotations; not other tools' formats) |
| Several contexts in one config | Yes | No | Partial | Partial | Not documented |
| Local port conflict detection | Yes (reports the holding process) | Partial (`:remotePort` picks a free port) | Not documented | Yes | Not documented |
| Platforms | Linux, macOS, Windows | Not documented | macOS, Linux, Windows | macOS, Linux, Windows | macOS, Linux, Windows |
| Licence | MIT | Not documented | Apache-2.0 | Apache-2.0 | GPL-3.0 |
| Language | Go | Not documented | Go | Go | Rust (Tauri, Ratatui) |
| Latest release (2026-10-02) | [Releases](https://github.com/lukaszraczylo/kportal/releases) | Not documented | v0.51.0 (2026-06-06) | v1.25.16 (2026-06-20) | v0.29.2 (2026-09-29) |
When another tool fits better:
- kubectl port-forward for a one-off forward. It needs no config file and ships with kubectl.
- k9s for general cluster browsing and its built-in benchmark.
- kubefwd to forward every service in a namespace with local DNS names.
- kftray for a tray GUI, shared JSON configs and UDP.
Kube Forwarder is a GUI-only forwarder. Its last release was 2019-08-14.
Sources:
- kubectl: [Use Port Forwarding to Access Applications in a Cluster](https://kubernetes.io/docs/tasks/access-application-cluster/port-forward-access-application-cluster/)
- k9s: <https://github.com/derailed/k9s> and <https://k9scli.io/topics/bench/>
- kubefwd: <https://github.com/txn2/kubefwd>
- kftray: <https://github.com/hcavarsan/kftray>
## Installation
### Homebrew (macOS) ### Homebrew (macOS)
@@ -60,29 +108,37 @@ kportal manages multiple Kubernetes port-forwards with an interactive terminal i
brew install --cask lukaszraczylo/taps/kportal brew install --cask lukaszraczylo/taps/kportal
``` ```
> **Note**: If you previously installed via `brew install lukaszraczylo/taps/kportal` (formula), uninstall first: If you installed the formula (`brew install lukaszraczylo/taps/kportal`) earlier, run `brew uninstall kportal` first.
> ```bash
> brew uninstall kportal
> ```
### Quick Install ### Quick install
```bash ```bash
curl -fsSL https://raw.githubusercontent.com/lukaszraczylo/kportal/main/install.sh | bash curl -fsSL https://raw.githubusercontent.com/lukaszraczylo/kportal/main/install.sh | bash
``` ```
The installer downloads `kportal-<version>-checksums.txt` from the same release and verifies the archive's SHA-256 before installing. If [`cosign`](https://github.com/sigstore/cosign) is on your `PATH`, the checksums file's keyless cosign signature is also verified against the shared-actions reusable workflow identity. The script downloads `kportal-<version>-checksums.txt` from the same release and verifies the archive SHA-256. If [`cosign`](https://github.com/sigstore/cosign) is on `PATH`, it also verifies the keyless signature of the checksums file.
| Variable | Effect | | Variable | Effect |
|----------|--------| |----------|--------|
| `DRY_RUN=1` | Download and verify only; do not install | | `DRY_RUN=1` | Download and verify only. Do not install. |
| `SKIP_COSIGN=1` | Skip cosign signature verification (SHA-256 is still enforced) | | `SKIP_COSIGN=1` | Skip the cosign check. The SHA-256 check still runs. |
| `INSTALL_DIR` | Target directory. Default `/usr/local/bin`. |
| `KPORTAL_VERSION` | Install a specific version, for example `1.2.3`, instead of the latest. |
| `COSIGN_CERT_IDENTITY_REGEXP` | Certificate identity to accept. Set it if you fork the release pipeline. |
### Manual Download Example:
Download binaries from the [releases page](https://github.com/lukaszraczylo/kportal/releases). ```bash
curl -fsSL https://raw.githubusercontent.com/lukaszraczylo/kportal/main/install.sh | DRY_RUN=1 bash
```
### Build from Source ### Manual download
The [releases page](https://github.com/lukaszraczylo/kportal/releases) has archives for Linux, macOS and Windows on amd64 and arm64. Archives are named `kportal-<version>-<os>-<arch>.tar.gz`, and `.zip` on Windows. Each release also has a `kportal-<version>-checksums.txt` file.
### Build from source
Go 1.26 or later is required (see `go.mod`).
```bash ```bash
git clone https://github.com/lukaszraczylo/kportal.git git clone https://github.com/lukaszraczylo/kportal.git
@@ -90,12 +146,13 @@ cd kportal
make build && make install make build && make install
``` ```
### Verifying Release Signatures `make install` copies the binary to `/opt/homebrew/bin` if that directory exists, otherwise to `~/.local/bin` on macOS and Linux.
All release checksums are signed with [cosign](https://github.com/sigstore/cosign) using keyless signing. To verify: ### Verifying release signatures
Release checksums are signed with [cosign](https://github.com/sigstore/cosign) using keyless signing. Download the checksums file and its sigstore bundle from the release, then run:
```bash ```bash
# Download the checksum file and its sigstore bundle from the release
cosign verify-blob \ cosign verify-blob \
--certificate-identity-regexp "^https://github\.com/lukaszraczylo/shared-actions/\.github/workflows/go-release\.yaml@refs/heads/main$" \ --certificate-identity-regexp "^https://github\.com/lukaszraczylo/shared-actions/\.github/workflows/go-release\.yaml@refs/heads/main$" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
@@ -103,7 +160,9 @@ cosign verify-blob \
kportal-<version>-checksums.txt kportal-<version>-checksums.txt
``` ```
## 🚀 Quick Start Replace `<version>` with the release version, for example `1.2.3`. Then check the archive against the verified checksums file with `shasum -a 256` or `sha256sum`.
## Quick start
Create `.kportal.yaml`: Create `.kportal.yaml`:
@@ -124,31 +183,40 @@ contexts:
port: 8080 port: 8080
localPort: 8080 localPort: 8080
alias: api alias: api
httpLog: true # Enable HTTP traffic logging httpLog: true
``` ```
Each `name` under `contexts` must exist in your kubeconfig. List them with `kubectl config get-contexts`.
Run: Run:
```bash ```bash
kportal kportal
``` ```
### Keyboard Controls ### Keyboard controls
Forwards list:
| Key | Action | | Key | Action |
|-----|--------| |-----|--------|
| `↑↓` / `j/k` | Navigate | | `Up` `Down` / `j` `k` | Move the selection |
| `Space` / `Enter` | Toggle forward | | `PgUp` `PgDn` / `Ctrl+u` `Ctrl+d` | Move by 10 rows |
| `n` | Add new forward | | `Space` / `Enter` | Toggle the forward |
| `e` | Edit forward | | `n` | Add a forward |
| `d` | Delete forward | | `e` | Edit the forward |
| `b` | Benchmark connection | | `d` | Delete the forward (asks to confirm) |
| `l` | View HTTP logs | | `b` | Benchmark the forward |
| `q` | Quit | | `l` | Open the HTTP log |
| `q` / `Ctrl+c` | Quit |
## 📖 Configuration In the add and edit wizard, `h` on the confirmation step toggles `httpLog`. Advanced `httpLog` keys you set in YAML stay unchanged.
### Basic Structure HTTP log keys are listed under [HTTP traffic logging](#http-traffic-logging).
## Configuration
### Basic structure
```yaml ```yaml
contexts: contexts:
@@ -162,56 +230,56 @@ contexts:
localPort: <local-port> localPort: <local-port>
alias: <display-name> # optional alias: <display-name> # optional
selector: <label-selector> # optional selector: <label-selector> # optional
httpLog: true # optional - enable HTTP logging httpLog: true # optional
``` ```
### Forward Options ### Forward options
| Field | Required | Description | | Field | Required | Description |
|-------|----------|-------------| |-------|----------|-------------|
| `resource` | Yes | Resource type and name (e.g., `service/postgres`, `pod/my-app`) | | `resource` | Yes | `service/<name>`, `pod/<name>`, or `pod` with `selector` |
| `protocol` | Yes | Protocol (`tcp`) | | `protocol` | No | `tcp`. No other value is accepted. |
| `port` | Yes | Remote port | | `port` | Yes | Remote port |
| `localPort` | Yes | Local port | | `localPort` | Yes | Local port |
| `alias` | No | Display name and mDNS hostname | | `alias` | No | Display name and mDNS hostname |
| `selector` | No | Label selector for pod resolution | | `selector` | With bare `pod` | Label selector. Not allowed with `pod/<name>`. |
| `httpLog` | No | Enable HTTP traffic logging (`true`/`false`) | | `httpLog` | No | `true`, or a map of options (see [HTTP traffic logging](#http-traffic-logging)) |
### Resource Formats ### Resource formats
| Format | Description | | Format | Description |
|--------|-------------| |--------|-------------|
| `service/name` | Service forwarding | | `service/name` | Forward to a service |
| `pod/name` | Direct pod by name | | `pod/name` | Forward to a pod by exact name, or by name prefix |
| `pod/prefix` | Pod by prefix (matches `prefix-*`) | | `pod` with `selector` | Forward to a pod picked by label selector |
| `pod` + `selector` | Pod by label selector |
| `deployment/name` | Deployment |
### Health Check Configuration Valid resource types are `pod` and `service`. To reach a deployment, use `pod` with a `selector`.
### Health checks and reliability
```yaml ```yaml
healthCheck: healthCheck:
interval: "3s" # Check frequency interval: "3s"
timeout: "2s" # Check timeout timeout: "2s"
method: "data-transfer" # tcp-dial or data-transfer method: "data-transfer" # or tcp-dial
maxConnectionAge: "25m" # Reconnect before k8s timeout maxConnectionAge: "25m"
maxIdleTime: "10m" # Detect idle connections maxIdleTime: "10m"
reliability: reliability:
tcpKeepalive: "30s" tcpKeepalive: "30s"
dialTimeout: "30s" dialTimeout: "30s"
retryOnStale: true retryOnStale: true
watchdogPeriod: "30s"
``` ```
Health check methods: The values above are the defaults from `.kportal.yaml`.
- `tcp-dial` - Fast TCP connection test
- `data-transfer` - Verifies tunnel functionality by attempting data read
Connection age reconnection only triggers when the connection is also idle, preventing interruption of active transfers like database dumps. - `tcp-dial` opens a TCP connection to the local port.
- `data-transfer` also tries to read data through the tunnel. It is the default.
- A connection past `maxConnectionAge` reconnects only when it is also idle, so a running transfer is not cut.
- `watchdogPeriod` is the interval of the check for hung workers.
### mDNS Hostnames ### mDNS
Enable mDNS to access forwards via `.local` hostnames:
```yaml ```yaml
mdns: mdns:
@@ -225,22 +293,22 @@ contexts:
- resource: service/postgres - resource: service/postgres
port: 5432 port: 5432
localPort: 5432 localPort: 5432
alias: prod-db # Accessible via prod-db.local:5432 alias: prod-db # reachable as prod-db.local:5432
``` ```
- Explicit `alias` becomes `<alias>.local` - `alias: prod-db` publishes `prod-db.local`.
- Without alias, hostname is generated from resource name (`service/redis` → `redis.local`) - Without an alias, `service/redis` publishes `redis.local`.
- Works on macOS (Bonjour) and Linux (avahi-daemon) - A bare `pod` with a selector and no alias is not published.
- Services register as `_kportal._tcp`.
Check registration:
Verify registration:
```bash ```bash
dns-sd -B _kportal._tcp local # macOS dns-sd -B _kportal._tcp local # macOS
avahi-browse -t _kportal._tcp # Linux avahi-browse -t _kportal._tcp # Linux
``` ```
### Table Columns ### Table columns
Choose which columns the forwards table shows, and in what order. The setting applies to the interactive UI and the `-verbose` table:
```yaml ```yaml
tui: tui:
@@ -252,90 +320,83 @@ tui:
- name: status - name: status
``` ```
- Valid names: `context`, `namespace`, `alias`, `type`, `resource`, `remote`, `local`, `status` - Valid names: `context`, `namespace`, `alias`, `type`, `resource`, `remote`, `local`, `status`.
- The list order is the render order, and unlisted columns are hidden - List order is render order. Unlisted columns are hidden.
- `width` is the maximum text width, from 1 to 200. If you omit it, the column keeps its default width - `width` is the maximum text width, 1 to 200. Without it, the column keeps its default width.
- If you omit `tui`, the table keeps its default columns and order - Without `tui`, the table keeps its default columns and order.
- Changes apply on hot-reload - The setting applies to the UI and to the `-v` table, and reloads with the file.
## Usage ## Usage
### Interactive Mode ### Command line
| Command or flag | Effect |
|-----------------|--------|
| `kportal` | Start the terminal UI. |
| `-c <file>` | Config file. Default `.kportal.yaml`. |
| `-v` | Verbose logging. In headless mode it sets the log level only. |
| `-headless` | No UI. Logs go to stderr. |
| `-check` | Validate the config and exit. |
| `-context <name>` | Forward only the named contexts. Repeatable and comma-separated. |
| `-log-format text\|json` | Log format. Default `text`. |
| `-convert <file>` | Convert a kftray JSON file to kportal YAML. |
| `-convert-output <file>` | Output of `-convert`. Default `.kportal.yaml`. |
| `-update` | Check for updates and exit. |
| `-version` | Print the version and exit. |
| `kportal generate` | Add forwards from a cluster. |
| `kportal completion` | Print or install a shell completion script. |
Each flag also accepts the double-dash form, for example `--check`.
### Interactive mode
```bash ```bash
kportal kportal
``` ```
### Verbose Mode ### Verbose mode
```bash ```bash
kportal -v kportal -v
``` ```
### Headless Mode Without `-headless`, `-v` prints a plain-text table that redraws. The table uses the same status words as the UI. When stdout is not a terminal, it draws `✓ Active`, `⋯ Starting`, `↻ Reconnecting` and `✗ Error`.
Run without TUI for scripting and automation: ### Headless mode
```bash ```bash
kportal -headless kportal -headless
```
Headless mode emits both structured and standard-library logs to stderr by default
(suitable for redirecting to a log file or systemd journal). The `-v` flag controls
log level (debug vs info), not destination.
Combines well with verbose mode for background operation:
```bash
kportal -headless -v 2>kportal.log & kportal -headless -v 2>kportal.log &
kportal -headless -log-format json
``` ```
### Validate Configuration Headless mode logs to stderr, so you can redirect it to a file or the systemd journal. `-v` sets the log level, not the destination.
### Validate configuration
```bash ```bash
kportal --check kportal -check
``` ```
### Custom Config File ### Custom config file
```bash ```bash
kportal -c /path/to/config.yaml kportal -c /path/to/config.yaml
``` ```
### Select Contexts ### Select contexts
By default every context in the config file is forwarded. `--context` limits the By default kportal forwards every context in the file. `--context` limits the run to the contexts you name:
run to the contexts you name, which is useful when the file describes several
clusters but you only work against one at a time:
```bash ```bash
kportal --context development-team-a kportal --context development-team-a
```
The flag is repeatable and accepts comma-separated values, so these are
equivalent:
```bash
kportal --context team-a --context team-b kportal --context team-a --context team-b
kportal --context team-a,team-b kportal --context team-a,team-b
``` ```
It composes with the other modes, and works well as a per-cluster shell alias: An unknown name is an error that lists the contexts in the file. Those are the `contexts[].name` values in `.kportal.yaml`, not every context in your kubeconfig.
```bash Local-port conflicts are only reported between forwards that can run together. With `--context`, contexts you never select together can reuse a `localPort`:
alias kp-a='kportal --context development-team-a'
kportal --context development-team-a -headless
```
An unknown context name is an error that lists the contexts the file defines, so
a typo fails loudly instead of silently forwarding nothing.
#### Reusing local ports across contexts
Local-port conflicts are only reported between forwards that could run at the
same time. Contexts that are never selected together may therefore reuse the
same `localPort`, so the same service keeps the same local port in every
cluster and the app you are developing never needs reconfiguring:
```yaml ```yaml
contexts: contexts:
@@ -357,15 +418,9 @@ contexts:
- <<: *rate-service # same local port, no conflict - <<: *rate-service # same local port, no conflict
``` ```
This requires passing `--context`. Without it every context is active, so the Without `--context`, or with both contexts named, the duplicate port is an error.
duplicate ports above would genuinely collide and are still reported as an
error - as is naming both contexts at once (`--context team-a,team-b`).
### Generate Forwards from a Cluster ### Generate
The `generate` subcommand discovers services in a Kubernetes context and lets you
interactively pick which ones to forward. Selected entries are appended to the
config file with consecutive local ports starting from a value you choose.
```bash ```bash
kportal generate --context=my-cluster kportal generate --context=my-cluster
@@ -375,97 +430,51 @@ kportal generate --context=my-cluster --dry-run
| Flag | Description | | Flag | Description |
|------|-------------| |------|-------------|
| `--context` | (required) Kubernetes context to scan | | `--context` | Required. Kubernetes context to scan. |
| `--config` | Path to kportal config file (default: `.kportal.yaml`) | | `--config` | Config file to append to. Default `.kportal.yaml`. |
| `--dry-run` | Print the planned forwards but do not modify the config | | `--dry-run` | Print the planned forwards. Do not change the config. |
The interactive flow has three steps: Steps:
1. **Namespaces** — multi-select with `space`, toggle-all with `a`, filter with `/`. 1. Namespaces: select with `space`, toggle all with `a`, filter with `/`.
2. **Services** — same controls; rows already present in the config are locked off, and non-TCP ports are skipped (UDP is not supported by kportal's forward layer). 2. Services: same keys. Services already in the config are locked. Non-TCP ports are skipped.
3. **Port assignment** — choose a starting local port (default `10000`, must be ≥ `1024`). Local ports are assigned consecutively in stable order, skipping any already in use. 3. Port assignment: choose a first local port (default `10000`, minimum `1024`). Ports are assigned in order and skip ports in use.
Press `enter` on the final step to save (or to print and exit when `--dry-run` is set), `b` to go back, or `esc` to cancel. Press `enter` on the last step to save (or print, with `--dry-run`), `b` to go back, `esc` to cancel.
## Status Indicators The `--dry-run` output has this form (example values):
| Indicator | Description | ```text
|-----------|-------------| [dry-run] Would add 2 forwards to .kportal.yaml
10000 → my-cluster/default/service/api:8080
10001 → my-cluster/default/service/postgres:5432
```
### Shell completion
```bash
kportal completion --shell zsh # print the script
kportal completion --shell zsh --install # install it
kportal completion --uninstall # remove it
```
Without `--shell`, kportal detects the shell. Supported shells: bash, zsh, fish.
## Status indicators
| Indicator | Meaning |
|-----------|---------|
| `● Active` | Connection healthy | | `● Active` | Connection healthy |
| `○ Starting` | Initial connection (10s grace period) | | `○ Starting` | First connection. Failed health checks show Starting for the first 10s. |
| `◐ Reconnecting` | Reconnecting after failure | | `◐ Reconnecting` | Reconnecting after a failure |
| `✗ Error` | Connection failed | | `✗ Error` | Connection failed |
| `○ Disabled` | Manually disabled | | `○ Disabled` | Turned off in the UI |
## Advanced Features ## Advanced features
### HTTP Traffic Logging ### HTTP traffic logging
Press `l` in the TUI to view real-time HTTP traffic for a selected forward. The log viewer shows: Press `l` on a forward that has `httpLog` set.
| Column | Description |
|--------|-------------|
| TIME | Request timestamp |
| METHOD | HTTP method (GET, POST, etc.) |
| STATUS | Response status code |
| LATENCY | Request duration |
| PATH | Request path |
**List view shortcuts:**
| Key | Action |
|-----|--------|
| `↑/↓` | Navigate entries |
| `Enter` | View request details |
| `g/G` | Jump to top/bottom |
| `a` | Toggle auto-scroll |
| `f` | Cycle filter mode (All → Non-2xx → Errors) |
| `/` | Search by path or method |
| `c` | Clear all filters |
| `q` | Close log viewer |
**Detail view:**
Press `Enter` on any entry to see full request/response details including:
- Request and response headers (alphabetically sorted)
- Request and response bodies
- Timing information and status codes
| Key | Action |
|-----|--------|
| `↑/↓` | Scroll content |
| `PgUp/PgDn` | Scroll by page |
| `g` | Jump to top |
| `c` | Copy response body to clipboard |
| `Esc/q` | Return to list |
**Body display features:**
- **JSON formatting** - JSON bodies are pretty-printed with syntax highlighting
- **Compression handling** - gzip/deflate content is automatically decompressed
- **Binary detection** - Binary content shows a placeholder instead of garbled data
**Filter modes:**
- **All** - Show all entries
- **Non-2xx** - Hide successful (2xx) responses
- **Errors** - Show only 4xx and 5xx responses
**Toggling per-forward logging:**
In the add/edit wizard, press `h` on the confirmation step to toggle `httpLog` on or
off for the current forward. The wizard preserves any advanced `httpLog` keys
(`logFile`, `includeHeaders`, `maxBodySize`, `filterPath`) you set in YAML.
**Header redaction:**
When `httpLog.includeHeaders: true` is set, sensitive header values are
automatically replaced with `[REDACTED]`. The header name is preserved so you can
see that an `Authorization` header was present without exposing its value. Redacted
headers include `Authorization`, `Cookie`, `Set-Cookie`, `X-Api-Key`,
`X-Auth-Token`, `X-Csrf-Token`, `Proxy-Authorization`, `X-Access-Token`, and any
header whose name contains `token`, `secret`, `password`, or `apikey`. This is
always on and cannot be disabled.
**Advanced configuration:**
```yaml ```yaml
forwards: forwards:
@@ -474,48 +483,88 @@ forwards:
localPort: 8080 localPort: 8080
httpLog: httpLog:
enabled: true enabled: true
includeHeaders: true # values of sensitive headers are redacted includeHeaders: true # sensitive values are redacted
maxBodySize: 65536 # bytes; 0 = unlimited maxBodySize: 65536 # bytes
filterPath: "/api/" # only log paths matching this substring filterPath: "/api/" # log only paths containing this text
logFile: "api.log" # append entries to a file in addition to the in-memory ring logFile: "api.log" # also append entries to this file
``` ```
### Connection Benchmarking `maxBodySize` defaults to 1 MB (1048576 bytes) when you omit it or set a value of 0 or less.
Press `b` in the TUI to benchmark a selected forward. Configure: List view columns: TIME, METHOD, STATUS, LATENCY, PATH.
- **URL Path** - Target endpoint (default: `/`) | Key | Action |
- **Method** - HTTP method (GET, POST, etc.) |-----|--------|
- **Concurrency** - Number of parallel workers | `Up` `Down` | Move |
- **Requests** - Total number of requests | `Enter` | Open details |
| `g` / `G` | Top / bottom |
| `a` | Toggle auto-scroll |
| `f` | Cycle filter: All, Non-2xx, Errors |
| `/` | Search path or method |
| `c` | Clear filters |
| `q` | Close |
Results include: The detail view shows sorted request and response headers, bodies and timing. JSON bodies are pretty-printed, gzip and deflate bodies are decoded, and binary bodies show a placeholder.
- Success/failure counts
- Min/Max/Avg latency
- P50/P95/P99 percentiles
- Throughput (requests/sec)
- Status code distribution
### Hot-Reload | Key | Action |
|-----|--------|
| `Up` `Down` / `PgUp` `PgDn` | Scroll |
| `g` | Top |
| `c` | Copy the response body |
| `Esc` / `q` / `Enter` | Back to the list |
Configuration changes are applied automatically. Manual reload: #### Sensitive header redaction
With `includeHeaders: true`, kportal replaces these header values with `[REDACTED]` and keeps the name: `Authorization`, `Cookie`, `Set-Cookie`, `X-Api-Key`, `X-Auth-Token`, `X-Csrf-Token`, `Proxy-Authorization`, `X-Access-Token`, and any name containing `token`, `secret`, `password` or `apikey` (any case). You cannot turn this off. Source: `internal/httplog/proxy.go`.
### Benchmarking
Press `b` on a forward. Set the URL path (default `/`), the HTTP method, the concurrency and the request count. The wizard starts at 10 workers and 100 requests.
The results screen has this layout (`x.xx` marks a measured value):
```text
Total Requests: 100
Successful: 100 (100.0%)
Failed: 0
Latency (ms)
Min: x.xx
Max: x.xx
Avg: x.xx
P50: x.xx
P95: x.xx
P99: x.xx
Throughput
Requests/sec: x.xx
Bytes read: n
Status Codes
200: n
```
Only 2xx responses count as successful.
### Hot reload
kportal reloads when the config file changes. To reload by hand:
```bash ```bash
kill -HUP $(pgrep kportal) kill -HUP $(pgrep kportal)
``` ```
### Port Conflict Detection New forwards start, removed forwards stop and unchanged forwards keep running. An invalid config is rejected and the previous one stays active.
kportal validates port availability at startup and during hot-reload, showing which process is using conflicting ports. ### Port conflict detection
Conflicts are only reported between forwards that could be running together. kportal checks that local ports are free at startup and on reload. The report names the process that holds a busy port. kportal finds it with `lsof` on Linux and macOS and `tasklist` on Windows. A reload that would bind a busy port is rejected and the previous config stays active.
With `--context`, forwards in contexts outside the selection are ignored, which
is what lets each context reuse the same `localPort` values - see
[Reusing local ports across contexts](#reusing-local-ports-across-contexts).
### Retry Strategy Conflicts are only reported between forwards that can run together. See [Select contexts](#select-contexts).
Exponential backoff: 1s → 2s → 4s → 8s → 10s (max). Retries continue indefinitely until connection succeeds. ### Retry strategy
Exponential backoff with 10% jitter: 1s, 2s, 4s, 8s, then 10s (cap). Retries continue until the connection succeeds.
## Migration from kftray ## Migration from kftray
@@ -523,88 +572,63 @@ Exponential backoff: 1s → 2s → 4s → 8s → 10s (max). Retries continue ind
kportal --convert configs.json --convert-output .kportal.yaml kportal --convert configs.json --convert-output .kportal.yaml
``` ```
## Signal Handling `-convert-output` defaults to `.kportal.yaml`.
- `Ctrl+C` / `SIGTERM` - Graceful shutdown ## Signal handling
- `SIGHUP` - Reload configuration
## 🐛 Troubleshooting | Signal | Effect |
|--------|--------|
| `Ctrl+C`, `SIGTERM` | Graceful shutdown |
| `SIGHUP` | Reload the configuration |
### Port Already in Use ## Troubleshooting
Port already in use:
```bash ```bash
lsof -i :<port> lsof -i :<port>
kill <pid> kill <pid>
``` ```
### Connection Refused Connection refused:
1. Verify pod is running: `kubectl get pods -n <namespace>` 1. Check the pod: `kubectl get pods -n <namespace>`
2. Verify port is correct: `kubectl describe pod <pod>` 2. Check the port: `kubectl describe pod <pod>`
3. Check service endpoints: `kubectl get endpoints <service>` 3. Check the service endpoints: `kubectl get endpoints <service>`
### Context Not Found Context not found: list your contexts with `kubectl config get-contexts`. kportal reports `context <name> not found in kubeconfig` when a `contexts[].name` value is not in your kubeconfig. Names with `@`, `.`, `:` or `/` (for example `admin@home` or an EKS ARN) are valid. If `--context` reports `unknown context`, the name is missing from `contexts[].name` in the config file.
The add wizard shows a namespace prompt instead of a list: the cluster refused or returned an empty namespace list. Type the namespace name.
## Development
Requires Go 1.26 or later, kubectl and access to a cluster.
```bash ```bash
kubectl config get-contexts make build # build the binary
``` make test # run tests
Context names containing `@`, `.`, `:`, or `/` (e.g. `admin@home`,
`[email protected]`, GKE dotted names, EKS ARNs) are accepted by the
config validator.
If `--context` reports `unknown context "..."`, the name does not match any
`contexts[].name` in the config file. The error lists the names that are
defined - note these are the contexts in your `.kportal.yaml`, which need not be
every context in your kubeconfig.
## 🔧 Development
### Prerequisites
- Go 1.23+
- Kubernetes cluster access
- kubectl configured
### Build
```bash
make build # Build binary
make test # Run tests
make all # fmt, vet, staticcheck, test make all # fmt, vet, staticcheck, test
make install # Install locally make install # install locally
``` ```
## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. Release history is in [CHANGELOG.md](CHANGELOG.md).
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
## Telemetry ## Telemetry
On startup this binary sends a single anonymous adoption ping — project name, On startup kportal sends one anonymous adoption ping: project name, version and timestamp. It has a 2-second timeout and cannot block startup.
version, timestamp; no identifiers, no command output, no payload contents.
Fire-and-forget with a 2-second timeout; cannot block startup or panic.
See **[oss-telemetry — Disabling telemetry](https://github.com/lukaszraczylo/oss-telemetry#disabling-telemetry)** To opt out, set `DO_NOT_TRACK=1`, `OSS_TELEMETRY_DISABLED=1` or `KPORTAL_DISABLE_TELEMETRY=1`. See [oss-telemetry](https://github.com/lukaszraczylo/oss-telemetry#disabling-telemetry) for the wire format and source.
for the exact wire format, source, and full opt-out documentation.
Quick opt-out: set any of `DO_NOT_TRACK=1`, `OSS_TELEMETRY_DISABLED=1`,
or `KPORTAL_DISABLE_TELEMETRY=1`.
## License ## License
MIT License - see [LICENSE](LICENSE). MIT. See [LICENSE](LICENSE).
## Acknowledgments ## Credits
- [Bubble Tea](https://github.com/charmbracelet/bubbletea) - Terminal UI framework [Bubble Tea](https://github.com/charmbracelet/bubbletea), [Lipgloss](https://github.com/charmbracelet/lipgloss), [client-go](https://github.com/kubernetes/client-go). [kftray](https://github.com/hcavarsan/kftray) was the inspiration.
- [Lipgloss](https://github.com/charmbracelet/lipgloss) - Terminal styling
- [client-go](https://github.com/kubernetes/client-go) - Kubernetes client
- [kftray](https://github.com/hcavarsan/kftray) - Inspiration
## Links ## Links
- [Website](https://lukaszraczylo.github.io/kportal) - [Website](https://lukaszraczylo.github.io/kportal)
- [Issues](https://github.com/lukaszraczylo/kportal/issues) - [Issues](https://github.com/lukaszraczylo/kportal/issues)
- [Releases](https://github.com/lukaszraczylo/kportal/releases) - [Releases](https://github.com/lukaszraczylo/kportal/releases)
- [Changelog](CHANGELOG.md)
+1175 -1071
View File
File diff suppressed because it is too large Load Diff