diff --git a/README.md b/README.md index e01f2fc..8798e62 100644 --- a/README.md +++ b/README.md @@ -14,34 +14,95 @@ kportal runs many Kubernetes port-forwards from one YAML file and shows their st ![kportal Screenshot](docs/kportal-screenshot.png) +Screenshot from kportal v0.2.17. The current key bar also lists `PgUp`/`PgDn`, `b` (Bench) and `l` (Logs). + Website: +## Contents + +- [Features](#features) +- [Comparison](#comparison-with-other-tools) +- [Installation](#installation) +- [Quick start](#quick-start) +- [Configuration](#configuration) +- [Usage](#usage) +- [Status indicators](#status-indicators) +- [Advanced features](#advanced-features) +- [Migration from kftray](#migration-from-kftray) +- [Signal handling](#signal-handling) +- [Troubleshooting](#troubleshooting) +- [Development](#development) +- [Telemetry](#telemetry) +- [License](#license) + ## Features | Area | What kportal does | |------|-------------------| | Forwards | Forwards to `service/`, `pod/` (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 `-verbose` table. | +| 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. | +| 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 `.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 values such as `Authorization` and `Cookie` are redacted. | +| 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. | +| 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. | -## Install +## Comparison with other tools -Homebrew (macOS): +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: and +- kubefwd: +- kftray: + +## Installation + +### Homebrew (macOS) ```bash brew install --cask lukaszraczylo/taps/kportal @@ -49,7 +110,7 @@ brew install --cask lukaszraczylo/taps/kportal If you installed the formula (`brew install lukaszraczylo/taps/kportal`) earlier, run `brew uninstall kportal` first. -Install script: +### Quick install ```bash curl -fsSL https://raw.githubusercontent.com/lukaszraczylo/kportal/main/install.sh | bash @@ -61,10 +122,23 @@ The script downloads `kportal--checksums.txt` from the same release and |----------|--------| | `DRY_RUN=1` | Download and verify only. Do not install. | | `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: the [releases page](https://github.com/lukaszraczylo/kportal/releases) has archives for Linux, macOS and Windows on amd64 and arm64. +Example: -Build from source (Go 1.26 or later, see `go.mod`): +```bash +curl -fsSL https://raw.githubusercontent.com/lukaszraczylo/kportal/main/install.sh | DRY_RUN=1 bash +``` + +### 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---.tar.gz`, and `.zip` on Windows. Each release also has a `kportal--checksums.txt` file. + +### Build from source + +Go 1.26 or later is required (see `go.mod`). ```bash git clone https://github.com/lukaszraczylo/kportal.git @@ -72,10 +146,13 @@ cd kportal make build && make install ``` -### Verify a release signature +`make install` copies the binary to `/opt/homebrew/bin` if that directory exists, otherwise to `~/.local/bin` on macOS and Linux. + +### 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 -# Download the checksums file and its sigstore bundle from the release cosign verify-blob \ --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" \ @@ -83,6 +160,8 @@ cosign verify-blob \ kportal--checksums.txt ``` +Replace `` 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`: @@ -107,13 +186,149 @@ contexts: httpLog: true ``` +Each `name` under `contexts` must exist in your kubeconfig. List them with `kubectl config get-contexts`. + Run: ```bash kportal ``` -## CLI +### Keyboard controls + +Forwards list: + +| Key | Action | +|-----|--------| +| `Up` `Down` / `j` `k` | Move the selection | +| `PgUp` `PgDn` / `Ctrl+u` `Ctrl+d` | Move by 10 rows | +| `Space` / `Enter` | Toggle the forward | +| `n` | Add a forward | +| `e` | Edit the forward | +| `d` | Delete the forward (asks to confirm) | +| `b` | Benchmark the forward | +| `l` | Open the HTTP log | +| `q` / `Ctrl+c` | Quit | + +In the add and edit wizard, `h` on the confirmation step toggles `httpLog`. Advanced `httpLog` keys you set in YAML stay unchanged. + +HTTP log keys are listed under [HTTP traffic logging](#http-traffic-logging). + +## Configuration + +### Basic structure + +```yaml +contexts: + - name: + namespaces: + - name: + forwards: + - resource: / + protocol: tcp + port: + localPort: + alias: # optional + selector: # optional + httpLog: true # optional +``` + +### Forward options + +| Field | Required | Description | +|-------|----------|-------------| +| `resource` | Yes | `service/`, `pod/`, or `pod` with `selector` | +| `protocol` | No | `tcp`. No other value is accepted. | +| `port` | Yes | Remote port | +| `localPort` | Yes | Local port | +| `alias` | No | Display name and mDNS hostname | +| `selector` | With bare `pod` | Label selector. Not allowed with `pod/`. | +| `httpLog` | No | `true`, or a map of options (see [HTTP traffic logging](#http-traffic-logging)) | + +### Resource formats + +| Format | Description | +|--------|-------------| +| `service/name` | Forward to a service | +| `pod/name` | Forward to a pod by exact name, or by name prefix | +| `pod` with `selector` | Forward to a pod picked by label selector | + +Valid resource types are `pod` and `service`. To reach a deployment, use `pod` with a `selector`. + +### Health checks and reliability + +```yaml +healthCheck: + interval: "3s" + timeout: "2s" + method: "data-transfer" # or tcp-dial + maxConnectionAge: "25m" + maxIdleTime: "10m" + +reliability: + tcpKeepalive: "30s" + dialTimeout: "30s" + retryOnStale: true + watchdogPeriod: "30s" +``` + +The values above are the defaults from `.kportal.yaml`. + +- `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 + +```yaml +mdns: + enabled: true + +contexts: + - name: production + namespaces: + - name: default + forwards: + - resource: service/postgres + port: 5432 + localPort: 5432 + alias: prod-db # reachable as prod-db.local:5432 +``` + +- `alias: prod-db` publishes `prod-db.local`. +- Without an alias, `service/redis` publishes `redis.local`. +- A bare `pod` with a selector and no alias is not published. +- Services register as `_kportal._tcp`. + +Check registration: + +```bash +dns-sd -B _kportal._tcp local # macOS +avahi-browse -t _kportal._tcp # Linux +``` + +### Table columns + +```yaml +tui: + columns: + - name: alias + width: 30 # optional, 1-200 + - name: resource + - name: local + - name: status +``` + +- Valid names: `context`, `namespace`, `alias`, `type`, `resource`, `remote`, `local`, `status`. +- List order is render order. Unlisted columns are hidden. +- `width` is the maximum text width, 1 to 200. Without it, the column keeps its default width. +- Without `tui`, the table keeps its default columns and order. +- The setting applies to the UI and to the `-v` table, and reloads with the file. + +## Usage + +### Command line | Command or flag | Effect | |-----------------|--------| @@ -122,32 +337,52 @@ kportal | `-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 ` | Forward only the named contexts. See [Select contexts](#select-contexts). | +| `-context ` | Forward only the named contexts. Repeatable and comma-separated. | | `-log-format text\|json` | Log format. Default `text`. | | `-convert ` | Convert a kftray JSON file to kportal YAML. | | `-convert-output ` | 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. See [Generate](#generate-forwards-from-a-cluster). | -| `kportal completion` | Print a shell completion script. | +| `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`. -Headless example: +### Interactive mode ```bash +kportal +``` + +### Verbose mode + +```bash +kportal -v +``` + +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`. + +### Headless mode + +```bash +kportal -headless kportal -headless -v 2>kportal.log & +kportal -headless -log-format json ``` -Completion: +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 -kportal completion --shell zsh # print the script -kportal completion --shell zsh --install # install it -kportal completion --uninstall # remove it +kportal -check ``` -Without `--shell`, kportal detects the shell. +### Custom config file + +```bash +kportal -c /path/to/config.yaml +``` ### Select contexts @@ -185,7 +420,7 @@ contexts: Without `--context`, or with both contexts named, the duplicate port is an error. -### Generate forwards from a cluster +### Generate ```bash kportal generate --context=my-cluster @@ -207,25 +442,25 @@ Steps: Press `enter` on the last step to save (or print, with `--dry-run`), `b` to go back, `esc` to cancel. -## Keys +The `--dry-run` output has this form (example values): -Forwards list: +```text +[dry-run] Would add 2 forwards to .kportal.yaml + 10000 → my-cluster/default/service/api:8080 + 10001 → my-cluster/default/service/postgres:5432 +``` -| Key | Action | -|-----|--------| -| `Up` `Down` / `j` `k` | Move the selection | -| `PgUp` `PgDn` / `Ctrl+u` `Ctrl+d` | Move by 10 rows | -| `Space` / `Enter` | Toggle the forward | -| `n` | Add a forward | -| `e` | Edit the forward | -| `d` | Delete the forward (asks to confirm) | -| `b` | Benchmark the forward | -| `l` | Open the HTTP log | -| `q` / `Ctrl+c` | Quit | +### Shell completion -In the add and edit wizard, `h` on the confirmation step toggles `httpLog`. Advanced `httpLog` keys you set in YAML stay unchanged. +```bash +kportal completion --shell zsh # print the script +kportal completion --shell zsh --install # install it +kportal completion --uninstall # remove it +``` -Status values: +Without `--shell`, kportal detects the shell. Supported shells: bash, zsh, fish. + +## Status indicators | Indicator | Meaning | |-----------|---------| @@ -235,95 +470,9 @@ Status values: | `✗ Error` | Connection failed | | `○ Disabled` | Turned off in the UI | -## Configuration +## Advanced features -### Forwards - -```yaml -contexts: - - name: - namespaces: - - name: - forwards: - - resource: / - protocol: tcp - port: - localPort: - alias: # optional - selector: # optional - httpLog: true # optional -``` - -| Field | Required | Description | -|-------|----------|-------------| -| `resource` | Yes | `service/`, `pod/`, or `pod` with `selector` | -| `protocol` | No | `tcp`. No other value is accepted. | -| `port` | Yes | Remote port | -| `localPort` | Yes | Local port | -| `alias` | No | Display name and mDNS hostname | -| `selector` | With bare `pod` | Label selector. Not allowed with `pod/`. | -| `httpLog` | No | `true`, or a map of options (see [HTTP log](#http-log)) | - -### Health checks and reliability - -```yaml -healthCheck: - interval: "3s" - timeout: "2s" - method: "data-transfer" # or tcp-dial - maxConnectionAge: "25m" - maxIdleTime: "10m" - -reliability: - tcpKeepalive: "30s" - dialTimeout: "30s" - retryOnStale: true - watchdogPeriod: "30s" -``` - -The values above are the defaults from `.kportal.yaml`. - -- `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. - -### mDNS - -```yaml -mdns: - enabled: true -``` - -- `alias: prod-db` publishes `prod-db.local`. -- Without an alias, `service/redis` publishes `redis.local`. -- A bare `pod` with a selector and no alias is not published. - -Check registration: - -```bash -dns-sd -B _kportal._tcp local # macOS -avahi-browse -t _kportal._tcp # Linux -``` - -### Table columns - -```yaml -tui: - columns: - - name: alias - width: 30 # optional, 1-200 - - name: resource - - name: local - - name: status -``` - -- Valid names: `context`, `namespace`, `alias`, `type`, `resource`, `remote`, `local`, `status`. -- List order is render order. Unlisted columns are hidden. -- `width` is the maximum text width, 1 to 200. Without it, the column keeps its default width. -- Without `tui`, the table keeps its default columns and order. -- The setting applies to the UI and to the `-verbose` table, and reloads with the file. - -### HTTP log +### HTTP traffic logging Press `l` on a forward that has `httpLog` set. @@ -335,11 +484,13 @@ forwards: httpLog: enabled: true includeHeaders: true # sensitive values are redacted - maxBodySize: 65536 # bytes, 0 = unlimited + maxBodySize: 65536 # bytes filterPath: "/api/" # log only paths containing this text logFile: "api.log" # also append entries to this file ``` +`maxBodySize` defaults to 1 MB (1048576 bytes) when you omit it or set a value of 0 or less. + List view columns: TIME, METHOD, STATUS, LATENCY, PATH. | Key | Action | @@ -353,22 +504,49 @@ List view columns: TIME, METHOD, STATUS, LATENCY, PATH. | `c` | Clear filters | | `q` | Close | -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. +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. | Key | Action | |-----|--------| | `Up` `Down` / `PgUp` `PgDn` | Scroll | | `g` | Top | -| `c` | Copy response body | +| `c` | Copy the response body | | `Esc` / `q` / `Enter` | Back to the list | -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`. This cannot be turned off. +#### Sensitive header redaction -### Benchmark +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`. -Press `b` on a forward. Set the URL path (default `/`), HTTP method, concurrency and request count. +### Benchmarking -### Reload +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: @@ -376,14 +554,33 @@ kportal reloads when the config file changes. To reload by hand: kill -HUP $(pgrep kportal) ``` -`Ctrl+C` and `SIGTERM` shut down cleanly. +New forwards start, removed forwards stop and unchanged forwards keep running. An invalid config is rejected and the previous one stays active. -### Import from kftray +### Port conflict detection + +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. + +Conflicts are only reported between forwards that can run together. See [Select contexts](#select-contexts). + +### Retry strategy + +Exponential backoff with 10% jitter: 1s, 2s, 4s, 8s, then 10s (cap). Retries continue until the connection succeeds. + +## Migration from kftray ```bash kportal --convert configs.json --convert-output .kportal.yaml ``` +`-convert-output` defaults to `.kportal.yaml`. + +## Signal handling + +| Signal | Effect | +|--------|--------| +| `Ctrl+C`, `SIGTERM` | Graceful shutdown | +| `SIGHUP` | Reload the configuration | + ## Troubleshooting Port already in use: @@ -399,7 +596,7 @@ Connection refused: 2. Check the port: `kubectl describe pod ` 3. Check the service endpoints: `kubectl get endpoints ` -Context not found: list your contexts with `kubectl config get-contexts`. 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. +Context not found: list your contexts with `kubectl config get-contexts`. kportal reports `context 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. @@ -429,3 +626,9 @@ MIT. See [LICENSE](LICENSE). ## Credits [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. + +## Links + +- [Website](https://lukaszraczylo.github.io/kportal) +- [Issues](https://github.com/lukaszraczylo/kportal/issues) +- [Releases](https://github.com/lukaszraczylo/kportal/releases)