{"id":50455599,"url":"https://github.com/beztebya666/k8s-view","last_synced_at":"2026-06-01T02:06:37.150Z","repository":{"id":356712894,"uuid":"1233753149","full_name":"beztebya666/k8s-view","owner":"beztebya666","description":"Fast, self-hosted Kubernetes web UI for multi-cluster ops — stream pod logs, exec, port-forward, edit YAML with live diff, rollout history \u0026 one-click rollback, Prometheus + metrics-server charts. Single Go binary, no agents, scales to 150k+ objects per cluster.","archived":false,"fork":false,"pushed_at":"2026-05-19T10:12:09.000Z","size":846,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-19T11:52:02.811Z","etag":null,"topics":["data-visualization","devops","devops-tools","docker","k8s","k8s-cluster","k8s-dashboard","k8s-ui","k8s-view","kubernetes","kubernetes-dashboard","kubernetes-ui","kubernetes-view","linux","monitoring","sre"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/beztebya666.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-09T10:08:56.000Z","updated_at":"2026-05-19T09:57:13.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/beztebya666/k8s-view","commit_stats":null,"previous_names":["beztebya666/k8s-view"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/beztebya666/k8s-view","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beztebya666%2Fk8s-view","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beztebya666%2Fk8s-view/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beztebya666%2Fk8s-view/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beztebya666%2Fk8s-view/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/beztebya666","download_url":"https://codeload.github.com/beztebya666/k8s-view/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beztebya666%2Fk8s-view/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33756614,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-01T02:00:06.963Z","response_time":115,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["data-visualization","devops","devops-tools","docker","k8s","k8s-cluster","k8s-dashboard","k8s-ui","k8s-view","kubernetes","kubernetes-dashboard","kubernetes-ui","kubernetes-view","linux","monitoring","sre"],"created_at":"2026-06-01T02:06:34.476Z","updated_at":"2026-06-01T02:06:37.144Z","avatar_url":"https://github.com/beztebya666.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# k8s-view\n\n**A streaming, multi-cluster Kubernetes dashboard. Single binary. Apache-2.0.**\n\n![k8s-view dashboard — pods list, topology graph, related ReplicaSets, live container logs](docs/screenshots/dashboard.png)\n\n`k8s-view` is a self-hosted Kubernetes UI built on a strict streaming model:\nevery resource list in the browser is fed by a `client-go` shared informer on\nthe backend and pushed to the frontend as binary MessagePack deltas over a\nsingle WebSocket. **No polling. No pagination.** A 100 000-pod namespace\nscrolls at 60 fps because both ends are virtualised.\n\nThe same binary serves the React UI, the REST API, the streaming WebSocket\nand the long-lived `exec`/`logs`/`port-forward` channels. Drop it on a host,\npoint it at a kubeconfig, and you're done.\n\n| | |\n|---|---|\n| **Latest release** | `v0.4.2` |\n| **License** | Apache-2.0 |\n| **GHCR image** | `ghcr.io/beztebya666/k8s-view:v0.4.2` |\n| **Docker Hub image** | `beztebya666/k8s-view:v0.4.2` |\n| **Build** | Go 1.22 + Node 20 |\n| **Compatible with** | Kubernetes 1.20 → 1.36 (`client-go` v0.31, dynamic discovery) |\n\n---\n\n## Table of contents\n\n- [Features](#features)\n- [Quick start](#quick-start)\n  - [Single binary](#single-binary)\n  - [Docker](#docker)\n  - [Kubernetes (Helm)](#kubernetes-helm)\n  - [Kubernetes (raw manifests)](#kubernetes-raw-manifests)\n- [Configuration](#configuration)\n  - [CLI flags \u0026 environment variables](#cli-flags--environment-variables)\n  - [Authentication](#authentication)\n  - [Multi-cluster](#multi-cluster)\n  - [Distributed mode](#distributed-mode)\n- [RBAC](#rbac)\n- [API surface](#api-surface)\n- [Observability](#observability)\n- [Architecture](#architecture)\n- [Building from source](#building-from-source)\n- [Development](#development)\n- [Security model](#security-model)\n- [Troubleshooting](#troubleshooting)\n- [Versioning \u0026 release policy](#versioning--release-policy)\n- [License](#license)\n\n---\n\n## Features\n\n**Real-time view of every resource type.**\n- Watch streams from the API server are forwarded to the browser as binary\n  MessagePack deltas. Status changes appear in single-digit milliseconds.\n- One shared informer per `(cluster, GVR)` is multiplexed to all subscribers,\n  so 50 open browser tabs cost the cluster the same as 1.\n- Built-in resources, CRDs, and any GVR the discovery client surfaces are\n  rendered the same way (list, detail, YAML edit, events, actions).\n- Per-resource warning sort: anything with a structural problem (failed\n  pull, crashloop, exit code, ProgressDeadlineExceeded, drained node, …)\n  bubbles to the top of every list with a clickable warning chip that\n  opens a verbatim cause tooltip.\n\n**Side-panel inline actions — operate without ever opening a terminal.**\n- Open any pod, deployment, statefulset, daemonset, replicaset, job or\n  cronjob in the right side-panel and trigger the most common kubectl\n  verbs from a single icon strip:\n  - **Scale** with a slider modal (Lens-style current/target readout).\n  - **Rollout restart** (`kubectl rollout restart` equivalent — patches\n    the template's `restartedAt` annotation, pods recycle gradually).\n  - **Rollout history \u0026 one-click rollback** for Deployments — every\n    owned ReplicaSet listed with image, replica counts and age; rollback\n    is gated by a side-by-side server-side YAML diff confirmation\n    (`kubectl rollout undo --to-revision=N` equivalent). `change-cause`\n    annotation is opt-in via a per-device toggle.\n  - **kubectl-describe** view with full event propagation across owners\n    and pods, plus a sortable **Related objects** panel (Name / Status /\n    Age columns; sort preference persists per device).\n  - **Trigger now** for CronJob (creates a one-shot Job from the\n    `jobTemplate`) and **Suspend / Resume** for the schedule itself.\n  - **Edit YAML** in a Monaco editor with a \"Review diff\" step before\n    server-side apply; **Delete** with a confirm modal (foreground /\n    background propagation). A distinct **Force delete**\n    (`--force --grace-period=0`) sits next to every Delete — row kebab,\n    bulk selection, and the detail-panel header.\n  - **Copy kubectl** popover that emits a context-prefixed command for\n    `get -o yaml`, `describe`, `logs`, `exec`, `port-forward`, etc.\n  - **Pin to favourites** so the resource appears in the sidebar across\n    every cluster tab.\n\n**Pod sessions in the bottom workspace pane.**\n- Independent tabs for **logs**, **exec** (interactive xterm.js shell),\n  **attach** to a running container, **port-forward**, **edit YAML**, and\n  **create from template** — they survive page navigation.\n- Logs panel: multi-container selection, server-side initial tail (50 →\n  20 000 lines), in-browser ring with a **Buffer** toggle to grow past\n  the tail size up to a hard cap, **timestamps** and **pod-name** column\n  toggles, **text-wrap**, **pretty-JSON** and **logfmt** toggles with\n  severity-aware line coloring, **previous container** dump, regex /\n  case-sensitive **search** with match counter and prev/next nav, filter\n  mode, **Pause** / **Resume**, **Clear**, **Following / Jump-to-present**\n  anchor, and a pop-out window to detach the tab into its own browser\n  window.\n- Cross-rollout pod history: when a pod ends and a new one is minted by\n  the same controller, the logs tab offers a one-click jump to the\n  predecessor's logs without losing the live tail.\n- Auto-reconnect with exponential backoff and a server keepalive, so\n  alive-but-quiet pods never get marked **Frozen** by mistake.\n\n**Multi-cluster, no-login session model.**\n- Reads every context in `$KUBECONFIG` on startup; switching clusters in\n  the UI re-uses the existing informer pool, no reload.\n- Add additional kubeconfigs at runtime — the UI ships an \"Add cluster\n  from kubeconfig\" import flow that persists the file per-identity and\n  re-registers every context on next start.\n- Each cluster gets a deterministic colour (FNV-1a hash of the name)\n  used everywhere the UI needs to disambiguate which cluster an action\n  applies to: the tab strip border, the cluster badge, the action chips.\n- **Environment tags \u0026 custom icons** — tag a cluster (e.g. `PROD`) and\n  the badge follows it onto every tab and the picker; give it an\n  uploaded image, an emoji, or a hue so production is unmistakable.\n- **Per-device session** — the dashboard issues a `kv_device` cookie on\n  first visit so two browsers / devices keep separate cluster lists\n  without a login wall. The legacy `~/.k8s-view/imported/` directory is\n  auto-adopted to the first device. SSO / LDAP providers can be layered\n  on top via the auth chain.\n\n**Cluster Overview \u0026 metrics.**\n- Per-cluster Overview page with workload counts, node status grid, and\n  CPU / Memory donut charts.\n- Pod-level **CPU / Memory / Network / Filesystem** time-series in the\n  side panel, with reference lines for `requests` and `limits` read from\n  the live spec.\n- Node-level **CPU / Memory / Disk** time-series on the node detail page —\n  Prometheus `node-exporter` joined to the node name, with a live\n  `metrics-server` fallback for CPU/Memory and the node's `allocatable`\n  budget drawn as a reference line.\n- **Prometheus auto-discovery** — the backend probes for a `Service`\n  matching `prometheus`/`kube-prometheus`/etc. and proxies PromQL when\n  found; falls back to `metrics-server` for CPU/Memory and to \"no metrics\"\n  cleanly when neither is installed.\n- Workload metrics aggregate across every pod in the controller's\n  pod-name pattern, so a Deployment chart shows the sum of its replicas\n  on both Prometheus and metrics-server backends.\n\n**Topology \u0026 policy visualisation.**\n- Service / Deployment / Pod **topology graph** in the side panel —\n  shows owner chains, label-selected backends, and cross-namespace\n  references at a glance; leaf Pod cards carry the running container\n  image tag so you read the deployed build straight from the graph.\n- **NetworkPolicy graph** that resolves ingress / egress rules into the\n  pods they actually permit, with a clickable matrix view.\n- **GitOps source surfacing** — when a resource is owned by Argo CD or\n  Flux, the side panel shows the source repo / path / target revision.\n\n**Navigation \u0026 layout.**\n- Browser-style **back / forward** history plus a Home jump in the top bar.\n- **Responsive shell** — the sidebar collapses to an off-canvas drawer and\n  the detail panel goes full-screen on phones / narrow windows; tables\n  drop secondary columns instead of forcing a horizontal scroll.\n- Quick-filter strips for **Pod status** and **Secret type**; a **Group\n  picker** scopes the API-resources and CRD pages; CRD rows expose an\n  \"Edit YAML / View definition\" action and the detail panel renders a\n  resource's `spec` as YAML.\n\n**Built for scale.**\n- Frontend uses `@tanstack/react-virtual` end-to-end — the DOM only\n  renders what's on screen, regardless of list size.\n- Backend uses `client-go` shared informers with tuned QPS / burst\n  (100 / 200) and configurable resync.\n- `--mode=api` + `--mode=worker` + Redis lets you scale the API\n  horizontally while a single worker fleet owns the watches per cluster.\n\n**Operations.**\n- `GET /api/v1/healthz` and `GET /api/v1/version` for probes.\n- Structured Zap logging with request-IDs, latency, status, bytes.\n- Container is hardened by default — non-root, read-only root FS,\n  dropped caps, `RuntimeDefault` seccomp.\n- No telemetry. No phone-home. No third-party SDKs in the binary.\n\n---\n\n## Quick start\n\n### Single binary\n\n```bash\nmake build           # builds frontend + backend into ./bin/k8s-view\n./bin/k8s-view       # binds :8080, uses $KUBECONFIG or ~/.kube/config\n```\n\nOpen http://localhost:8080.\n\n### Docker\n\nThe image is published to two registries — pick whichever is closer to\nyou. Both carry the same digest.\n\n```bash\n# GitHub Container Registry\ndocker pull ghcr.io/beztebya666/k8s-view:v0.4.2\n\n# Docker Hub\ndocker pull beztebya666/k8s-view:v0.4.2\n```\n\n`:latest` is also available on both and tracks the newest release.\n\nFor a local k3s/minikube kubeconfig that points at a loopback address,\nhost-network the container so `https://127.0.0.1:6443` resolves correctly:\n\n```bash\ndocker run --rm -it \\\n  --network host \\\n  --security-opt label=disable \\\n  -v ${HOME}/.kube/config:/home/app/.kube/config:ro \\\n  ghcr.io/beztebya666/k8s-view:v0.4.2\n```\n\nFor a kubeconfig that points at a routable API server, bridge networking\nwith a port publish is fine:\n\n```bash\ndocker run --rm -it \\\n  -v ${HOME}/.kube/config:/home/app/.kube/config:ro \\\n  -p 8080:8080 \\\n  ghcr.io/beztebya666/k8s-view:v0.4.2\n```\n\n`make docker-run` wraps the host-network form for local development.\n\n### Kubernetes (Helm)\n\n```bash\nhelm install k8s-view ./deploy/helm \\\n  --namespace k8s-view --create-namespace\nkubectl -n k8s-view port-forward svc/k8s-view 8080:80\n```\n\nThe chart provisions a `ServiceAccount`, a `ClusterRole`/`ClusterRoleBinding`,\na hardened `Deployment` (non-root, read-only root FS, dropped capabilities,\nseccomp `RuntimeDefault`), a `Service`, and an optional `Ingress`. Override\n`rbac.rules` to scope down what the UI can do — see [RBAC](#rbac) below.\n\n### Kubernetes (raw manifests)\n\nIf you don't run Helm:\n\n```bash\nkubectl apply -f deploy/k8s/all-in-one.yaml\nkubectl -n k8s-view port-forward svc/k8s-view 80:80\n```\n\n---\n\n## Configuration\n\n### CLI flags \u0026 environment variables\n\nEvery flag has an environment-variable counterpart so the same configuration\nworks in shells, systemd units and pod specs.\n\n| Flag | Env | Default | Purpose |\n|---|---|---|---|\n| `--listen` | `K8SVIEW_LISTEN` | `:8080` | HTTP listen address. If the port is busy, the server falls back to a free one and logs the switch. |\n| `--open` | `K8SVIEW_OPEN` | auto | Open the dashboard in the default browser on startup. On for local runs, off in-cluster. |\n| `--kubeconfig` | `KUBECONFIG` | `~/.kube/config` | Kubeconfig path. Ignored when `--in-cluster` is set. |\n| `--in-cluster` | `K8SVIEW_IN_CLUSTER` | auto | Use the pod's ServiceAccount instead of a kubeconfig. Auto-detected by the presence of `/var/run/secrets/kubernetes.io/serviceaccount/token`. |\n| `--default-cluster` | `K8SVIEW_DEFAULT_CLUSTER` | `current-context` | Cluster selected on first load. |\n| `--log-level` | `K8SVIEW_LOG_LEVEL` | `info` | `debug` \\| `info` \\| `warn` \\| `error`. |\n| `--resync-seconds` | `K8SVIEW_RESYNC_SECONDS` | `0` | Informer resync period. `0` = pure delta stream, no periodic full sync. |\n| `--allow-origins` | `K8SVIEW_ALLOW_ORIGINS` | `*` | Comma-separated CORS allowlist. |\n| `--basic-auth-user` | `K8SVIEW_BASIC_AUTH_USER` | _(unset)_ | If set, requires HTTP basic-auth. |\n| `--basic-auth-pass` | `K8SVIEW_BASIC_AUTH_PASS` | _(unset)_ | Paired with `--basic-auth-user`. Constant-time comparison. |\n| `--mode` | `K8SVIEW_MODE` | `all-in-one` | `all-in-one` \\| `api` \\| `worker`. See [distributed mode](#distributed-mode). |\n| `--redis` | `K8SVIEW_REDIS` | _(unset)_ | Redis address used by the distributed cache. |\n\n`k8s-view --help` prints the same list at runtime.\n\n### Authentication\n\nIdentity in `k8s-view` is **pluggable**, with no required login wall. The\nauth middleware composes one or more providers from `internal/auth/`:\n\n1. **Device cookie (default).** A `kv_device` cookie is issued on first\n   visit so two browsers / devices keep separate cluster lists. Imported\n   kubeconfigs and per-user preferences hang off the device ID. Best for\n   single-tenant or trusted-network deployments where \"open and use\" is\n   the desired UX.\n2. **HTTP basic auth.** Set `--basic-auth-user` / `--basic-auth-pass` (or\n   the `K8SVIEW_BASIC_AUTH_*` env vars). The credentials are compared\n   with `crypto/subtle.ConstantTimeCompare`. Pairs well with the device\n   cookie: basic-auth gates entry, the cookie disambiguates devices.\n3. **OIDC / LDAP.** First-class providers ship in\n   [`internal/auth/oidc.go`](internal/auth/oidc.go) and\n   [`internal/auth/ldap.go`](internal/auth/ldap.go) — when configured,\n   the resolved identity replaces the device cookie as the primary\n   subject for per-identity manager scoping.\n4. **Authenticating reverse proxy.** For SSO frontends you already run\n   (`oauth2-proxy`, Pomerium, Cloudflare Access, an Ingress with an OIDC\n   filter), deploy `k8s-view` behind it — the chart's Ingress\n   annotations are written to make this straightforward.\n\n**Authorisation** is delegated to the Kubernetes API server: the\ndashboard never has more privilege than the ServiceAccount or\nkubeconfig user it runs as. Lock down what the UI can do by editing\nthe bound RBAC rules — see [RBAC](#rbac) below.\n\n### Multi-cluster\n\nThree sources of clusters are merged at startup, in this order of\npreference:\n\n1. **In-cluster credentials** when `--in-cluster` is set. Yields a single\n   cluster named `in-cluster`.\n2. **Every context in `KUBECONFIG`.** Each context becomes a separately\n   addressable cluster.\n3. **Imported kubeconfigs** under `~/.k8s-view/imported/`. The UI's\n   \"Add cluster from kubeconfig…\" action persists uploads here, and the\n   manager re-reads the directory on every start.\n\nSwitching clusters in the UI is free: each `(cluster, GVR)` informer is\ncreated on first subscription and stays warm for subsequent visits.\n\n### Distributed mode\n\nThe default `--mode=all-in-one` is a single binary that owns its own\ninformers and serves the UI. Two horizontal-scale modes exist for large\ndeployments:\n\n| Mode | Owns informers | Serves UI / API | Needs Redis |\n|---|---|---|---|\n| `all-in-one` | yes | yes | no |\n| `api` | no | yes — reads cache | yes |\n| `worker` | yes — pushes to cache | no | yes |\n\nRun a single fleet of `worker` pods with cluster credentials, and a\nhorizontally-scaled fleet of `api` pods that fan out the WebSocket to\nbrowsers.\n\n---\n\n## RBAC\n\nThe bundled `ClusterRole` is intentionally permissive (`*`/`*`/`*`) because\nthe dashboard is meant to be a full-featured kubectl replacement. Override\nit for environments that need read-only or scoped access.\n\n**Read-only example** (`values.yaml`):\n\n```yaml\nrbac:\n  create: true\n  rules:\n    - apiGroups: [\"*\"]\n      resources: [\"*\"]\n      verbs: [\"get\", \"list\", \"watch\"]\n    - apiGroups: [\"\"]\n      resources: [\"pods/log\"]\n      verbs: [\"get\"]\n```\n\n**Scope to a single namespace** by replacing the `ClusterRoleBinding` with\na `RoleBinding`. Cluster-scoped resources (Nodes, PVs, CRDs, …) will\ndisappear from the UI but everything namespaced will keep working.\n\n`k8s-view` performs no client-side permission checks: when an action is\ndenied by the API server, the resulting error is surfaced verbatim in a\ntoast / inline error.\n\n---\n\n## API surface\n\nAll endpoints are mounted under `/api/v1`. Resource paths use the standard\n`group/version/resource` triple, with the literal `core` for the core API\ngroup.\n\n| Method | Path | Purpose |\n|---|---|---|\n| `GET` | `/healthz` | Liveness/readiness probe. |\n| `GET` | `/version` | Build version + commit. |\n| `GET` | `/clusters` | List configured clusters. |\n| `POST` | `/clusters/import` | Upload a kubeconfig; persisted to `~/.k8s-view/imported/`. |\n| `POST` | `/clusters/{name}/select` | Mark a cluster as the default for new tabs. |\n| `GET` | `/{cluster}/api-resources` | Discovery dump for the cluster. |\n| `GET` | `/{cluster}/namespaces` | List namespaces (cached, 60 s). |\n| `GET` | `/{cluster}/stream` | WebSocket: subscribe/unsubscribe to GVR streams; binary MessagePack deltas. |\n| `GET`/`PUT`/`DELETE` | `/{cluster}/resource/{group}/{version}/{resource}[/ns/{namespace}]/{name}` | Generic CRUD via the dynamic client. `PUT` is server-side apply. |\n| `POST` | `/{cluster}/apply` | Server-side apply for arbitrary YAML. |\n| `GET` | `/{cluster}/pods/{namespace}/{name}/logs` | WebSocket: tail logs (per-container). |\n| `GET` | `/{cluster}/pods/{namespace}/{name}/exec` | WebSocket: interactive shell via SPDY. |\n| `GET` | `/{cluster}/pods/{namespace}/{name}/attach` | WebSocket: attach to a running container. |\n| `GET` | `/{cluster}/pods/{namespace}/{name}/portforward` | WebSocket: port-forward. |\n| `POST` | `/{cluster}/pods/{namespace}/{name}/evict` | Pod eviction sub-resource. |\n| `POST` | `/{cluster}/scale/{group}/{version}/{resource}/ns/{namespace}/{name}` | Scale subresource. |\n| `POST` | `/{cluster}/restart/{group}/{version}/{resource}/ns/{namespace}/{name}` | Rollout restart annotation. |\n| `GET` | `/{cluster}/rollouts/{namespace}/{name}` | Deployment revision history (owned ReplicaSets sorted by `deployment.kubernetes.io/revision`). |\n| `POST` | `/{cluster}/rollouts/{namespace}/{name}/rollback` | Roll a Deployment back to `{revision: N, changeCause?: \"...\"}` — kubectl-rollout-undo equivalent. |\n| `POST` | `/{cluster}/nodes/{name}/cordon` \\| `/uncordon` \\| `/drain` | Node lifecycle. |\n| `POST` | `/{cluster}/nodes/{name}/shell` | Spawn an ephemeral privileged node-shell pod; blocks until its container is Running. |\n| `POST` | `/{cluster}/nodes/{name}/kubeadm` | Control-plane `kubeadm` op (`certs-renew` / `upgrade`). Control-plane-only; requires a typed confirm token. |\n| `DELETE` | `/{cluster}/node-shell/{namespace}/{name}` | Tear down a node-shell pod. |\n| `GET` | `/{cluster}/events/{namespace}` | Recent events. |\n| `GET` | `/{cluster}/metrics/pods/{namespace}` | metrics-server pod metrics. |\n| `GET` | `/{cluster}/metrics/nodes` | metrics-server node metrics. |\n| `GET` | `/{cluster}/prometheus/{info,query,query_range}` | Prometheus auto-discovery + proxy. |\n\nEvery JSON error response carries `{ \"error\": \"...\", \"at\": \"\u003cRFC3339\u003e\" }`.\nEvery request is logged with `request_id`, method, path, status, duration,\nbytes and remote address.\n\n---\n\n## Observability\n\n| Endpoint | Use |\n|---|---|\n| `GET /api/v1/healthz` | Liveness + readiness. Returns `{\"status\":\"ok\"}` once the cluster manager has finished initialising. |\n| `GET /api/v1/version` | Returns `{\"version\":\"…\",\"commit\":\"…\"}` (set at build time via `-ldflags -X main.version=…`). |\n| Logs | Structured JSON via Zap. Request log keys: `request_id`, `method`, `path`, `status`, `duration`, `bytes`, `remote`, `user_agent`, `cluster` (where applicable). 5xx logged at `warn`, 4xx at `info`, 2xx at `debug`. |\n\nThe Prometheus integration is **for the dashboard's own charts**, not for\nemitting `k8s-view` metrics. The dashboard auto-discovers a Prometheus\n`Service` in the cluster and switches the Cluster Overview from\nmetrics-server to PromQL when one is found.\n\n---\n\n## Architecture\n\n```\n┌──────────── browser ──────────────┐         ┌──────────── k8s-view backend ──────────┐\n│  React + Vite                     │  WS     │  HTTP router (chi)                     │\n│  ─────────────────────────────    │ ◀────▶ │   ├─ CORS · request-log · recoverer    │\n│  • TanStack Virtual lists         │ binary  │   └─ optional basic-auth                │\n│  • Monaco YAML editor             │ deltas  │                                        │\n│  • xterm.js — exec / logs         │         │  Cluster Manager                        │\n│  • per-tab Lens-style workspace   │         │   ├─ kubeconfig + in-cluster + imports │\n│  • per-cluster colour identity    │         │   ├─ shared informers (one per GVR)    │\n│  • Floating Create resource FAB   │         │   ├─ delta broadcaster (msgpack)       │\n│  • Bottom pane: logs/exec/yaml/   │         │   └─ action handlers                   │\n│    create/terminal/port-forward   │         │       (apply / delete / scale / exec)  │\n└───────────────────────────────────┘  HTTP   │                                        │\n                                       ────▶ │  embed.FS → React build                 │\n                                              └────────────────────────────────────────┘\n                                                              ▲\n                                                              │ client-go (watch / SPDY / metrics)\n                                                              ▼\n                                                     ┌──────────────────┐\n                                                     │ Kubernetes APIs  │\n                                                     │ (+ optional      │\n                                                     │   Prometheus)    │\n                                                     └──────────────────┘\n```\n\n**Key design choices**\n\n- **One watch per `(cluster, GVR)`, fanned out to N subscribers.** A 50-tab\n  team observing the same cluster generates 1× the watch traffic of a\n  single tab.\n- **Binary MessagePack deltas** keep WebSocket frames small and free the\n  frontend from JSON.parse pressure when 10k objects update at once.\n- **`embed.FS` for the React build.** No separate static-file server; the\n  dashboard ships as a single binary you can `scp` and run.\n- **No global HTTP timeout** because logs / exec / port-forward are\n  long-lived. Per-handler context cancellation handles disconnects.\n\n---\n\n## Building from source\n\n**Prerequisites**\n\n- Go 1.22+\n- Node 20+\n- Docker (only for `make docker` / `make docker-run`)\n- Helm 3 (only for chart installs)\n\n**Targets**\n\n```bash\nmake build          # frontend + tidy + backend → ./bin/k8s-view\nmake frontend       # React build only → internal/web/dist\nmake backend        # Go build only (assumes frontend dist exists)\nmake run            # build + ./bin/k8s-view\nmake docker         # docker build → $(DOCKER_IMG)\nmake docker-run     # docker run with host networking + your kubeconfig\nmake helm-template  # helm template …/deploy/helm\nmake helm           # helm install k8s-view ./deploy/helm --create-namespace\nmake tidy           # go mod tidy\nmake clean          # rm bin/ + frontend dist\n```\n\nThe version baked into the binary is set at build time:\n\n```bash\nmake build VERSION=v0.4.2\n./bin/k8s-view --help        # the help banner reflects the build version\ncurl -s :8080/api/v1/version # {\"version\":\"0.4.2\",\"commit\":\"\u003cshort-sha\u003e\"}\n```\n\n---\n\n## Development\n\n### Frontend\n\n```bash\ncd frontend\nnpm install\nnpm run dev                  # vite on :5173, /api proxied to :8080\nnpm run lint                 # tsc --noEmit\n```\n\nThe Vite dev server proxies `/api` to `http://localhost:8080` by default\n(override with `K8SVIEW_API`).\n\n### Backend\n\n```bash\ngo run ./cmd/k8sview --log-level=debug\n```\n\nThe frontend's embedded copy under `internal/web/dist` is what gets baked\ninto release binaries. During development the Vite dev server takes over,\nso you don't need to rebuild the frontend on every change.\n\n### Repository layout\n\n```\ncmd/k8sview/             entrypoint\ninternal/\n  api/                   chi router, REST handlers, WS streams (logs/exec/pf)\n  clusters/              cluster manager, informers, kubeconfig import\n  config/                CLI flags + env vars\n  log/                   zap logger\n  web/                   embed.FS for the React build\nfrontend/                React + Vite + TS + Tailwind\ndeploy/\n  helm/                  Chart, values, templates\n  k8s/                   all-in-one.yaml (no Helm needed)\n```\n\n---\n\n## Security model\n\n- **Identity** is the cluster's responsibility. `k8s-view` never asks the\n  user for credentials and only acts as the ServiceAccount or kubeconfig\n  user it was started with.\n- **Authorisation** is enforced by the Kubernetes API server. The bundled\n  RBAC is permissive by default; restrict it for production.\n- **Container hardening** (Helm chart and `all-in-one.yaml`):\n  - `runAsNonRoot: true`, `runAsUser: 65532`\n  - `readOnlyRootFilesystem: true`\n  - `allowPrivilegeEscalation: false`\n  - `capabilities: { drop: [ALL] }`\n  - `seccompProfile: { type: RuntimeDefault }`\n- **Imported kubeconfigs** are written to `~/.k8s-view/imported/` with the\n  process user's permissions. In-cluster deployments should disable the\n  import path or restrict it via an authenticating proxy if multi-tenant\n  use is in scope.\n- **Logs / exec / port-forward** stream over WebSocket and inherit the\n  same authorisation as the underlying Kubernetes sub-resources.\n- **No telemetry.** `k8s-view` does not phone home, does not collect\n  usage, and has no third-party SDKs in the binary.\n\n---\n\n## Troubleshooting\n\n**The UI shows \"offline\" and the logs say `dial tcp 127.0.0.1:6443: connect: connection refused`.**\nYou're running in Docker with bridge networking against a kubeconfig that\npoints at the host's loopback. Re-run with `--network host`, or change the\nkubeconfig server address to one reachable from inside the container.\n\n**Backend exits with `failed to initialise cluster manager`.**\nOut-of-date binary. As of v0.3.0 a missing kubeconfig is non-fatal — the\nmanager logs a warning and starts with the imported clusters only.\n\n**`Import is not available yet`** in the \"Add cluster from kubeconfig\"\nmodal. The frontend is talking to a backend that doesn't expose\n`POST /api/v1/clusters/import` (likely a stale binary). Rebuild\n(`make build`) and restart.\n\n**Pod logs page shows \"Frozen / Reconnect\" while the pod is alive.**\nCheck that the pod actually still has a recent log line — quiet pods used\nto false-trigger the freeze indicator. v0.3.0 keepalives + reconnect\nbackoff fix this; if you still see it, capture the WebSocket frames and\nfile an issue.\n\n**100k-row list scrolls but actions feel sluggish.**\nConfirm `--resync-seconds=0` so the backend isn't doing periodic full\nre-syncs on top of the watch stream, and that `--mode=all-in-one` isn't\nsharing a node with a noisy neighbour.\n\n---\n\n## Versioning \u0026 release policy\n\n- Semantic versioning. Breaking changes to the API surface or to\n  configuration flags bump the minor version while we're pre-1.0.\n- The container tag, the Helm chart `appVersion`, and the binary's\n  `--version` output are kept in sync.\n- The default branch is always shippable; tagged releases are what's\n  promoted to `ghcr.io` and Docker Hub.\n\n### CI/CD pipeline\n\nTwo GitHub Actions workflows, no credentials in the repo — everything\nsensitive comes from **Actions Secrets** only.\n\n| Workflow | Trigger | Output |\n|---|---|---|\n| [`ci.yml`](.github/workflows/ci.yml) | every PR \u0026 push to `main` | builds the frontend + backend as a sanity gate; on `main` also overwrites a single **rolling `:beta`** multi-arch image (no per-commit tags — `:beta` is always replaced in place) |\n| [`release.yml`](.github/workflows/release.yml) | tag `v*` **or** *Run workflow* | multi-arch `:vX.Y.Z` + `:latest` images **and** a GitHub Release with Linux/macOS/Windows binaries (amd64 + arm64) + `SHA256SUMS.txt` |\n\nThe `:beta` tag is how testers run the work-in-progress **without ever\ntouching the `:latest` / `:vX.Y.Z` images real users run** — `main` keeps\nmoving `:beta`, the stable tags only move on an explicit release.\n\n**One-time setup** (repository → Settings → Secrets and variables → Actions):\n\n| Secret | Purpose | Required? |\n|---|---|---|\n| `GHCR_TOKEN` | push to GHCR (GitHub Container Registry) | required |\n| `DOCKERHUB_TOKEN` | Docker Hub access token (**not** the account password) | optional — Docker Hub is mirrored only when this is set |\n\n\u003e `GHCR_TOKEN` is a GitHub Personal Access Token with the `write:packages`\n\u003e scope. The built-in `GITHUB_TOKEN` is **not** used for GHCR here: the\n\u003e container package pre-dates the workflow, and a pre-existing package does\n\u003e not automatically grant the repo's `GITHUB_TOKEN` write access. A scoped\n\u003e PAT side-steps that. (Alternatively, grant the repo Write access under the\n\u003e package's *Manage Actions access* and switch the login back to\n\u003e `GITHUB_TOKEN` — then no secret is needed.)\n\u003e\n\u003e `DOCKERHUB_TOKEN`: create at Docker Hub → *Account Settings → Personal\n\u003e access tokens* with **Read \u0026 Write** permission. Never the account password.\n\n**Cutting a release (the \"say go\" flow):**\n\n```bash\n# Option A — tag it\ngit tag v0.5.0 \u0026\u0026 git push origin v0.5.0\n\n# Option B — no terminal needed\n# GitHub ▸ Actions ▸ release ▸ Run workflow ▸ version: v0.5.0\n```\n\nEither path builds and publishes `:v0.5.0` + `:latest` everywhere and\nattaches the binaries to the Release. Bump the in-tree version strings\n(`cmd/k8sview/main.go`, `Makefile`, `frontend/package.json`,\n`deploy/helm/Chart.yaml`, `deploy/k8s/all-in-one.yaml`, this README) in the\ncommit you tag so `--version` matches the artifact.\n\n---\n\n## License\n\nApache License 2.0 — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeztebya666%2Fk8s-view","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbeztebya666%2Fk8s-view","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeztebya666%2Fk8s-view/lists"}