Architecture
- 1. The module map
- 2.
kweblens-core— cluster access, no web concerns - 3.
kweblens-web— one slice per surface - 4. The rules that produced this shape
- 4.1. Every surface is a slice over a core access service
- 4.2. The
ClusterRegistryowns every client lifecycle - 4.3. Cluster-scoped state registers a
ClusterClientListener - 4.4. Building a client does not connect
- 4.5. Clusters are ids, and
defaultis only a fallback - 4.6. In the front end, logic lives in
.tsand rendering in.vue
kweblens is one runnable Spring Boot application built out of a small reactor of Maven modules. The split is not decorative: it is a dependency direction. Cluster access lives in a module that knows nothing about HTTP, the web module turns that access into surfaces, and the SPA is a separate build that lands on the web module’s classpath as static resources.
1. The module map
| Module | What it is | License |
|---|---|---|
|
The cluster access layer — the fabric8 client registry, resource projection, health checks, logs, exec, metrics, port-forward, schema. No web concerns. |
Apache-2.0 |
|
The Vue 3 + Vite + TypeScript SPA. Built with |
— |
|
The runnable app: JSON API, the SPA, the MCP server, security and audit. One |
AGPL-3.0 |
|
A dependency-light cluster inspector (picocli). It resolves a kubeconfig and does not build a
client, so it takes fabric8’s |
Apache-2.0 |
|
On-demand operational and connectivity tasks. Its tests are tagged |
AGPL-3.0 |
|
Nothing is published to Maven Central, by decision. kweblens is an application — its
surface is a screen, a terminal and an MCP endpoint, not an API anyone compiles against — so a
release is a git tag plus the three runnable jars attached to the GitHub release, and the
signing and central-publishing plugins have been removed from the POM rather than left wired
to a path nothing takes. The |
1.1. Reactor shape and the profile trick
The top-level <modules> list contains only the two publishable libraries. The application is
added by an activeByDefault profile:
-
default(active in every ordinary build, including CI) addskweblens-ui,kweblens-webandkweblens-it. -
releaseis requested explicitly, which deactivatesdefaultand therefore drops the application from the reactor — an app does not belong on Maven Central. -
dockeris also explicit, so it has to re-addkweblens-uiandkweblens-webin order to build the image at all.
The consequence to remember: any new -P profile that needs the application must list
<module>kweblens-web</module> itself, or the app silently vanishes from the build.
2. kweblens-core — cluster access, no web concerns
| Package | What lives there |
|---|---|
|
|
|
|
|
The deterministic workload / network / storage / config checks. One implementation, shared by the dashboard, the diagnose pass and the MCP tools — so a verdict cannot differ depending on who asked. |
|
One access service per capability: cluster events, log follows, exec sessions, metrics, port-forwards, and the cluster’s own OpenAPI v3 schema. |
|
|
Nothing here imports Spring MVC. That is what makes the access layer testable against an in-JVM
mock API server, and what lets kweblens-it reuse it without dragging a web server along.
3. kweblens-web — one slice per surface
Each user-facing surface is a directory under org.alexmond.kweblens.web:
api/-
The JSON API and its
ProblemDetailerror mapping — one controller per surface, plusListProjection(what a list row is allowed to contain),ListJson, andSseKeepAlive. ui/-
SpaController, which forwards/,/uiand/ui/to the built SPA’sindex.html. security/-
SecurityConfigandAuditService. nav/-
NavCatalog, the declarative category → kind → list-route registry (39 built-in kinds across 8 static categories), andClusterNavService, which discovers CRDs, promotes a Gateway category at runtime when the Gateway API CRDs are present, and adds the cluster’s Vertical Pod Autoscaler kinds to the declared Autoscaling category when theirs are. mcp/-
The
@Toolbeans andMcpConfig, plusToolRedaction— the boundary that strips Secret values before anything leaves for an inference log. helm/-
The jhelm-backed release surface.
exec/-
Exec over WebSocket.
files/-
The pod file browser, off by default.
search/-
Global search —
SearchService,SearchKinds,SearchRanking. diag/-
The diagnostics panel’s capability report.
ai/-
DiagnoseService(LLM enrichment, inert without a key) andRemediationService(not AI-gated). sim/-
The in-JVM cluster simulator used for development, CI and performance work.
config/-
ClusterBootstrap, which seeds the ambient kubeconfig at startup.
/actuator/{health,info,metrics,prometheus} is exposed alongside them.
4. The rules that produced this shape
These are standing constraints, not history. Each one has already been a bug when it was broken.
4.1. Every surface is a slice over a core access service
A web/<area>/ slice composes, projects and serves. It never reimplements cluster access. That
is why adding logs, exec, port-forward, Helm, metrics and the detail drawer did not each grow
their own way of talking to a cluster — and why a fix in health/ is a fix everywhere the
verdict is shown.
4.2. The ClusterRegistry owns every client lifecycle
Never construct a KubernetesClient in a controller or a service. Ask the registry:
require(id) or client(id). Everything is addressed by cluster id — never by passing a
raw client around — and re-registering an id closes the previous client.
Two consequences that have each cost a bug:
-
A client built before it is handed over is owned by nobody. Between
new KubernetesClientBuilder().build()and the registry’sregister()returning, no one will close it, so every failure path in between has to. The registry’sadopt()guard exists for exactly that window; a stranded client keeps its own Vert.x runtime alive. -
Closing the client does not release everything the cluster held. A port-forward’s listening socket is bound by kweblens on the kweblens host, not by the cluster, so closing the client kills the tunnel and leaves the port bound. A cached OpenAPI document describes an API server that the id may no longer point at.
4.3. Cluster-scoped state registers a ClusterClientListener
The hook is ClusterRegistry.addClientListener, and it fires on removal and on re-point —
both are "anything derived from the old client is dead now". PortForwardService and
SchemaService both use it. Anything new that caches or binds per cluster must do the same.
The dependency deliberately runs one way: holders register themselves with the registry, and the registry knows nothing about them.
4.4. Building a client does not connect
new KubernetesClientBuilder().build() only resolves configuration; the first API call is what
reaches the cluster. This is why ClusterBootstrap can register every kubeconfig context at
startup for the cost of a map entry, and why the test suite needs no live cluster — and also why
a bad kubeconfig fails lazily, on first use rather than at boot.
4.5. Clusters are ids, and default is only a fallback
ClusterBootstrap registers one cluster per kubeconfig context, with the context name as the
id. A six-context kubeconfig registers six clusters. default is used only when there is no
readable kubeconfig at all (an in-cluster ServiceAccount), when the file declares no contexts, or
when it cannot be parsed. Never assume default exists — resolve ids from GET /api/v1/clusters.
4.6. In the front end, logic lives in .ts and rendering in .vue
Behaviour is written as plain TypeScript modules and composables (src/.ts,
src/composables/use.ts, and per-component *.ts files beside their .vue), so it is testable
without a DOM. The .vue file renders the result. That is why the front-end test suite runs in
the Maven gate with no browser: sorting, column fitting, palette matching, diff derivation,
watch batching and the rest are all functions, not components.
See Development for the gates that enforce this and the instruments used to measure the running app.