README.md
Rama main · réplica de lectura del repositorio original.
# Spinoza
[](https://github.com/sophotechlabs/spinoza/actions/workflows/go.yaml)
[](https://github.com/sophotechlabs/spinoza/actions/workflows/frontend.yaml)
[](https://github.com/sophotechlabs/spinoza/actions/workflows/repo.yaml)
[](https://github.com/sophotechlabs/spinoza/actions/workflows/go.yaml)
[](https://github.com/sophotechlabs/spinoza/actions/workflows/frontend.yaml)
[](LICENSE)
A self-hosted Kubernetes GUI: Go backend with client-go informers, React frontend, one binary. Runs as a browser tab or a desktop window. [spinoza.tech](https://spinoza.tech)
## Install
macOS and Linux:
```sh
curl -fsSL https://spinoza.tech/install.sh | sh
```
Windows, in PowerShell:
```powershell
irm https://spinoza.tech/install.ps1 | iex
```
The binary goes to `~/.local/bin`, or `/usr/local/bin` as root, and on Windows to `%LOCALAPPDATA%\Programs\spinoza`, which is added to your user `PATH`. `SPINOZA_INSTALL_DIR` overrides all three. Checksums are verified before anything is written. The desktop app lands in `/Applications` on macOS, your launcher on Linux, and an `app` subdirectory on Windows, unless `SPINOZA_SKIP_APP=1`.
```sh
spinoza --open # browser
open -a Spinoza # desktop window, macOS
```
Archives are on the [releases page](https://github.com/sophotechlabs/spinoza/releases): tarballs for Linux and macOS, zips for Windows. `SPINOZA_VERSION=v1.22.0` pins the installer.
Nothing is code-signed, so Windows SmartScreen warns on first run: More info, then Run anyway.
Set `SPINOZA_VERIFY_ATTESTATION=1` and both installers check the GitHub build provenance of what they downloaded before writing it. It needs `gh` installed and signed in, so it is off by default.
To remove it, on macOS and Linux:
```sh
curl -fsSL https://spinoza.tech/install.sh | SPINOZA_UNINSTALL=1 sh
```
On Windows, in PowerShell:
```powershell
$env:SPINOZA_UNINSTALL=1; irm https://spinoza.tech/install.ps1 | iex
```
Either takes back the binary, the desktop app and its launcher entry, and on Windows the `PATH` entry too. Your settings and kubeconfigs are left alone, and so is the directory the binary sat in.
Needs a kubeconfig. Upgrades, rollbacks and debug containers call `helm` and `kubectl`.
## Every type discovery reports
One informer-backed view per type, so CRDs need no per-type code. Snapshot, then deltas over a WebSocket keyed by uid. Field-aware filter completed from what the cluster reported, namespace scope, failing-object badges, bulk restart and delete.

## Clusters
Open as many as you like. Each is a tab with its own state, health and history, named and coloured
however you want, grouped, and remembered or forgotten between runs. Anything destructive on a
protected cluster asks for the name typed out.

The fleet view answers one question across every open cluster at once: checks, issues, search, Helm
releases and GitOps, without switching tab.
## Checks
128 checks over security, reliability and efficiency, decided against the live cluster rather than a
parsed manifest, and labelled PSS baseline, PSS restricted, NSA/CISA or CIS Kubernetes Benchmark where
a framework covers them.
They read workloads, RBAC, networking, storage and your own custom resources, and warn about APIs a
coming release removes and certificates about to expire.

Read them by check, or by control: CIS Kubernetes Benchmark alongside PSS and
NSA/CISA, with what fails each control, and — for the controls no rule answers,
or that nothing reading a live cluster could — that nothing answers them, and
why. Take them away as CSV, as JSON, or as SARIF that whatever already reads
SARIF will take unchanged, with a fingerprint per finding so a second run
deduplicates against the first.
Findings rank by how far the problem reaches, not by rule severity alone. Mute a rule with one of your
own, take a baseline and compare a cluster against it, or write a check as a CEL expression. A rule
sees the cluster the audit read through `cluster.list(group, resource)` and `cluster.get(...)`,
compares quantities with `quantity()`, reads a certificate's dates with `x509.notAfter()`, and has
the string, list, set, math, regex and encoder libraries of CEL.
## Issues
What is broken now, ranked and self-clearing as the cluster recovers, paged, and available across
every open cluster at once. The timeline beside it records what the cluster did.

## Topology
Open the graph around one object and it draws what owns it, what it owns, what routes to it and what
configures it. Folded by default; the whole cluster is one click away.

## Compare across clusters
Any object against another context side by side, or a whole kind at once with a verdict per object:
same, differs by so many lines, or present on only one side. Both sides lose status, managedFields and
the rest of what the server assigns, so what differs is what you authored.

## History
Every change Spinoza made, kept per cluster, with what changed and when.

## GitOps
Flux and Argo CD side by side: cluster sync, controller health, per-kind lists, status tiles. Reconcile, suspend and resume Flux objects. Sync and refresh Argo Applications.

The dependency graph is laid out by what manages what. Argo's is the app-of-apps, ApplicationSets above what they generate. A node opens in the drawer with its actions.

## Helm without the binary
Releases read from Helm's own storage, either driver. Chart and app version, what your repos offer, revision, status, values, notes, rendered manifest, history. OCI end to end. Upgrade behind a server-rendered manifest diff; rollback and uninstall from the same panel. A Flux-owned release links to its HelmRelease instead of offering an upgrade button.

Selecting a release docks its detail as a panel. Drag it to any side, resize it, collapse it; the placement persists, and `release` and `releaseNs` in the URL carry it through a reload or a back button. Clicking a rendered resource opens that object in its own table with the row selected and the drawer open, while the release stays docked underneath.

## Inspect and edit
Metadata, conditions, events and live YAML in Monaco. Schema-aware completion from the cluster's own OpenAPI. Apply sends the object back with the resourceVersion you read, so a draft built on a read the cluster has moved past is refused rather than written over it. Delete names the object you inspected by its UID, so a replacement that reuses the name is left alone. An edited draft survives a background reload, and the browser's Back button asks before it discards one.

## Port-forwarding
Any pod or service on localhost, no kubectl. Forwards survive navigation, listed and stopped from one panel.

## Rollout revisions
What a Deployment, StatefulSet or DaemonSet rolled out, which revision is
running now, what differs between any two of them once the server's own fields
are stripped, and a way back to one. Undo asks the apiserver first, is greyed
out with the reason when the cluster would refuse it, and asks for the name
typed out on a protected cluster.
## Reserved against used
What every namespace and workload asks the scheduler for against what it
actually uses, ranked by what could be given back. Usage comes from Prometheus
over a window where there is one, from metrics-server otherwise, and where
nothing measured it the view says so rather than showing a zero.
## Write actions
Scale, rollout restart, undo, cordon, uncordon, drain. Drain shows its eviction plan before it runs: what it evicts, what it leaves, what it blocks and why. Protected clusters require the object name typed out.

## Also
- **Cluster overview**: server version, node readiness, allocatable against live usage, pods by phase, recent warning events.
- **Exec** over `v5.channel.k8s.io` into xterm. Shell-less images get an ephemeral debug container, on a verdict cached per image digest.
- **Logs** per container. Pausing follow stops the scroll, not the stream.
- **Metrics**: metrics-server in the tables, CPU and memory history from Prometheus through the apiserver proxy. `--prometheus namespace/service:port` overrides discovery. With no Prometheus to ask, spinoza samples metrics-server itself while the window is open and says so on the chart.
- **Update check**: asks spinoza.tech once per run whether a newer release exists, and offers the install command if so. Automatic replacement is disabled because the remote installer script has no independent authenticity proof. `SPINOZA_UNSAFE_SELF_UPDATE=true` restores the legacy download-and-execute path for compatibility and lets mutable remote code run with your user's privileges.
- **Saved views**: a filter, its columns, its namespace and its kind kept under a name and reached from the command palette. In cluster mode they are each person's own, and an admin can publish one for everybody.
- **Kubeconfigs** added by path, referenced in place, never copied or merged. Contexts grouped per file, listed in `kubeconfigs.json`. `--kubeconfig PATH` replaces the default lookup for one run.
- **MCP server**: `spinoza-mcp`, a separate binary giving an agent 21 tools and 3 resources over one cluster.
- **Node shell**: a root shell in the node's own namespaces, off until you turn it on. It asks the apiserver whether you may create the pod first, and the pod carries a two-hour deadline.
- **Secrets** are masked on screen, one key revealed at a time, and the reveal drops the moment you select something else. The values are in the response the browser received, so anyone allowed to read a Secret can also read it from the developer tools.
- **Traffic**: a workload graph from Cilium Hubble flow metrics, read through Prometheus.
- **Nine themes**, Borg by default, contrast-gated in CI, plus your own as JSON. Screenshots here are Borg.
- **Window size**: built for 1280×720 and up, checked in CI at 1280×720 and 1440×900 and at 200% browser zoom. Narrower than 1280 the workspace scrolls sideways rather than reflowing, and a phone is out of scope. A fresh profile opens the Focused preset — one workspace, side and bottom docks closed; Console opens every dock, and Settings → Panels switches between them. A layout you arranged yourself is never overwritten.
## Cluster mode
The same binary serves one cluster to a team: run it as a pod, put it behind
your ingress, and point it at your Keycloak, or any other OIDC provider, the way
you would point Grafana at one.
```sh
helm upgrade --install spinoza deploy/helm/spinoza \
--namespace spinoza --create-namespace \
--set publicURL=https://spinoza.example.com \
--set auth.mode=oidc \
--set auth.oidc.issuerURL=https://keycloak.example.com/realms/main \
--set auth.oidc.clientID=spinoza \
--set auth.oidc.clientSecret=... \
--set auth.adminGroups='{platform-admins}'
```
Signing in is authorization code with PKCE, the ID token verified against the
provider's JWKS, and a signed session cookie. Groups from the token map to
viewer, editor and admin. The secure chart default uses a read-only workload
service account with no Secret access and no impersonation power. Deployments
that need Kubernetes RBAC per person can enable impersonation for explicit
usernames and groups. Shared feeds then check the signed-in person's exact
resource access before returning cached data and recheck it while the feed is
open. Proxy-header auth and back-channel logout are both there.
It reports on itself the way anything else in the cluster does: `/metrics` in
Prometheus exposition, json logs, a request name on every answer that also
appears in the log line for that request, and `/readyz` that says which of the
catalog, the caches and the state store it is still waiting for — so a new pod
does not take traffic while its tables would be empty, and a restart costs a
reconnect rather than an error. Every change it makes goes out as a structured
log line as well as into the trail, and can be exported. Terminal sessions can
be recorded when a deployment asks for that. The checks can run on a timer and
post a summary when posture moves.
Full guide, including what changes when it runs this way: [docs/cluster-mode.md](docs/cluster-mode.md).
Which account acts in each mode, what a greyed-out feature is waiting for, and the next step: [docs/modes.md](docs/modes.md).
## Security
On your own machine it binds loopback only and rejects non-local Host or Origin. Every route and both WebSockets require the token minted for that run, as the `X-Spinoza-Token` header, a `token` query parameter, or the cookie the page keeps. `--token-file PATH` writes it at mode 0600. Exits when the last view closes.
Outbound: your apiserver, the chart repos you configured, and one request per run to spinoza.tech asking whether a newer release exists. In cluster mode the update check is off, and the only extra outbound is your identity provider.
## Develop
Toolchain pinned in `mise.toml` (Go 1.27, Node 24); `mise install` gets them. The container image builds with the same Go.
```sh
just deps # frontend dependencies, once
just run # build the frontend and the binary, then start
just dev-api # Go server on :34115
just dev-web # Vite, proxying to the API
just rund # build and open the desktop app
```
Vite serves its own `index.html` without the token, so open `http://localhost:5173/?token=<the token dev-api printed>`.
The binary embeds the built frontend; `just stub-assets` writes a placeholder for the Go-only recipes.
Every test recipe takes an optional test name and an optional path:
```sh
just test-be TestScaleRejectsNegative ./internal/actions/...
just test-fe 'renders the empty state' test/components/History.test.tsx
just test-e2e 'the view says what it is for' specs/history.spec.ts
```
Either argument can be given alone. A filtered Go run writes no coverage profile.
`just check` before pushing. `just ci` runs everything CI does: cross-compilation, the coverage gate, `govulncheck`, dead-code and unused-dependency checks, a bundle-size budget, secret scanning and SAST.
## Architecture
Discovery builds a resource catalog. Each subscribed GVR gets a dynamic informer whose cache is projected into table rows; a WebSocket sends one snapshot then deltas keyed by uid. Informers strip managedFields before caching. Exec and port-forwarding get their own transports: a binary WebSocket carrying the Kubernetes exec channel protocol, and a process-global forward registry independent of any request.
The React app is embedded with `embed.FS`. One HTTP and WebSocket server backs both the browser tab and the desktop window, and you move between them mid-session.
Built on client-go v0.36, so Kubernetes 1.35 to 1.37 by skew policy. Runs in production against k3s v1.36.
## License
[FSL-1.1-ALv2](LICENSE): use, modify and redistribute it for anything except a commercial product that competes with Spinoza. Each release turns into Apache 2.0 two years after it ships.
It comes as is, with no warranty of any kind and no liability on Sophotech s.r.o. for what it does. `spinoza --license` prints the full text, the release archives carry it, and the installers leave a copy on disk.