Skip to content

Repository files navigation

CI OpenSSF Scorecard CodeQL npm Runtime dependencies Go TypeScript License Open Issues

krm-stream

Efficient live Kubernetes views for browser applications, with optional editing.

krm-stream helps applications watch Kubernetes resources while controllers and other users keep changing them. Its headless TypeScript client adds connection lifecycle, recovery and a live resource store, reading native Kubernetes collections through a host proxy or projected views from the gateway, and reconciles incoming changes with local drafts when a page needs editing. Its gateway delivers a defined resource view, withholds selected values, suppresses irrelevant updates and optionally shares upstream watches.

Your application supplies authentication, authorization policy, Kubernetes credentials, UI and writes. The browser client has zero runtime dependencies and chooses no UI framework.

Start with the watch

Kubernetes already provides a change feed. krm-stream adds a browser lifecycle and resource-view contract, reducing work at different points:

Capability Benefit Boundary
Scope Watch a resource kind, namespace, name or allowed label selector The host authorizes the scope; a selector does not grant access
Projection and redaction Deliver the fields a view needs; full/spec views withhold Kubernetes Secret values Secret key paths and change revisions remain visible; arbitrary sensitive CRD fields are not automatically redacted
Update suppression Avoid downstream events when projected content and redaction records are unchanged The gateway still receives upstream changes; fewer events do not guarantee a current write version
Optional watch sharing Use one upstream watch for matching scopes on the same shared backend Every subscriber is authorized separately and receives its own snapshot and updates

Choose a view explicitly:

View Delivered content Updates suppressed
krm-full/v1 (default) Resource including status, with Secret values withheld Bookkeeping-only changes
krm-spec/v1 Full view with status omitted Bookkeeping-only and status-only changes
krm-raw/v1 Secret values included when host policy permits Bookkeeping-only changes

All three remove metadata.managedFields and the last-applied-configuration annotation. krm-raw/v1 is still a projection. A hidden Secret rotation produces a redaction update in full/spec.

Each current gateway connection starts with a complete snapshot, then follows visible changes. On reconnect, a fresh snapshot repairs missed changes and deletes; resources are pruned only when it completes. This is a live state feed: intermediate updates may be coalesced. See watching resources.

Choose a source

Start with the resource state and guarantees the page needs; framing is an implementation detail.

Source What the page receives Connector
Native through a host proxy Original authorized resources, with the shared client lifecycle and store connectNativeWatch: LIST, then WATCH; a reconnect resumes the WATCH, and only an expired history lists again; edits are written back through the same proxy
Gateway Projected, redacted or suppressed views and optional upstream watch sharing connectResourceStream, delivered over SSE

Native is the straightforward starting point for hosts that already proxy Kubernetes. It delivers what the proxy returns, Secret values and machinery fields included; it provides no projection, redaction, suppression or watch sharing. Both connectors share connection state, errors, cancellation, bounded recovery and state events. Use a separate store per source, scope and login identity, and never fall back from a refused gateway view to native access.

Watch native resources through a host proxy

import {
  LiveResourceStore, readOnlyPolicy, applyStreamEvent,
  connectNativeWatch, nativeCollectionURL,
} from "@configbutler/krm-stream";

const store = new LiveResourceStore(readOnlyPolicy);
const stopRendering = store.subscribe(() => renderResources(store));
const connection = connectNativeWatch(
  // → /k8s/apis/apps/v1/namespaces/app/deployments?labelSelector=tier%3Dweb
  nativeCollectionURL("/k8s", {
    group: "apps", version: "v1", resource: "deployments", namespace: "app", labelSelector: "tier=web",
  }),
  event => applyStreamEvent(store, event),
);
const stopConnection = connection.subscribe(state => renderConnection(state.status));
connection.closed.catch(reportApplicationError);

// On view disposal:
stopRendering();
stopConnection();
connection.close();

The host mounts the API server's paths under a base such as /k8s and owns credentials, routing and which collections a user may read. The connector lists the complete collection, delivers it as a snapshot and becomes live once the WATCH from the collection's resourceVersion is accepted. A dropped or ended watch resumes from the last event applied, without a new snapshot, so drafts stay and missed changes arrive as events. HTTP or in-stream 410 (expired history) lists again within the bounded retry budget, and the store prunes only when that snapshot completes; 401, 403 and other 4xx are terminal. The connector does not paginate: a LIST that returns a continuation token is refused. The native viewer example runs this against kubectl proxy.

Watch a gateway view

import {
  LiveResourceStore, readOnlyPolicy, applyStreamEvent,
  connectResourceStream, resourceStreamURL,
} from "@configbutler/krm-stream";

const store = new LiveResourceStore(readOnlyPolicy);
const stopRendering = store.subscribe(() => renderResources(store));
const connection = connectResourceStream(
  resourceStreamURL("/resource-stream/v1", {
    target: "production", version: "v1", resource: "configmaps", namespace: "app",
  }),
  event => applyStreamEvent(store, event),
);
renderConnection(connection.state.status);
const stopConnection = connection.subscribe(state => renderConnection(state.status));
connection.closed.catch(reportApplicationError);

// On view disposal:
stopRendering();
stopConnection();
connection.close();

Viewers use LiveResourceStore(readOnlyPolicy); a dedicated read-only store is deferred. The connector delivers state events independently of editing. It uses same-origin cookies by default, exposes connection state and bounded retries, and stops on terminal refusals. Apply each event synchronously. The client README explains lifecycle and errors. For a browser without a bundler, the same API is available in one file through @configbutler/krm-stream/bundle.

Add editing when the page needs it

Use new LiveResourceStore() for an editor. It keeps the last delivered server object separate from the person's draft. Incoming changes update untouched fields, preserve local edits and record conflicts when both sides changed the same editable field differently.

For example, someone changes a Deployment's image while an autoscaler changes its replicas. The replicas follow the server and the image edit stays. If another person changes that image to a different value, the editor keeps the local value and exposes the disagreement for review.

const store = new LiveResourceStore(); // use this store in the connection setup for an editor
store.setValue(uid, ["data", "message"], "hello"); // ConfigMap field edit
store.conflicts(uid); // disagreements to resolve before Save
const intent = store.captureSave(uid); // detached { uid, resourceVersion, patch }
// Your save controller submits this intent after review while the connection is live.

The default policy allows spec, labels, annotations, data and stringData; status, immutable metadata and redacted paths remain read-only. Under every policy, metadata.managedFields and the last-applied annotation stay read-only too. A host can narrow the policy for its form.

The intended Save flow is explicit: capture the patch, UID and resource version together; have the host authorize and validate it; apply a conditional merge PATCH; then observe the projected result through the stream or a guarded projected read. Preserve typing made after Save. A version rejection can occur without any field conflict, because suppressed updates still advance Kubernetes versions. The current recovery is a guarded read, review and another deliberate Save. A native store follows the same flow through its host proxy: the browser sends the merge PATCH with the captured UID and version inside it, and the proxy validates it before forwarding.

A dirty draft, an accepted write and application progress are separate states. A successful PATCH can precede its watch observation, and neither proves a workload has finished rolling out.

Use the editor state model for reconciliation, conflict resolution and arrays, and saving edits safely for the complete host-owned write contract. The conditional-save example executes that contract, and the native editor example its native counterpart.

How it fits today

flowchart LR
  api["Kubernetes API"]
  proxy["Your native proxy<br/>Credentials, routing and write checks"]
  gateway["Go gateway<br/>Scopes, views and optional sharing"]
  connector["Fetch connector<br/>State events and recovery"]
  store["Resource store<br/>Live state and optional drafts"]
  ui["Your list, viewer or form"]
  save["Your save endpoint<br/>Authorize, validate and conditionally PATCH"]
  api -->|"LIST and WATCH"| proxy
  proxy -->|"Native watch JSON"| connector
  api -->|"Snapshot and watch"| gateway
  gateway -->|"SSE"| connector
  connector --> store
  store --> ui
  ui -->|"Local edits"| store
  ui -->|"Captured save intent"| save
  save --> api
  ui -->|"Native conditional PATCH"| proxy
  proxy --> api
Loading

A page uses one source per store: the native proxy or the gateway. The gateway runs inside your Go application. Per-user backends let Kubernetes authorize the caller's reads; a shared backend uses a service identity and requires checks for each subscriber. Sharing reduces duplicate upstream work; access controls and host limits govern who can consume it. Gateway upstream continuation and improved save progress during suppressed churn are proposed work. New browser connections receive a fresh snapshot under the current protocol.

Start here

If you already have a Go host, follow adopting krm-stream. For a ready-made host, krm-foyer integrates sign-in, sessions, native API proxying and krm-stream gateway hosting. The host's dependency version determines which APIs are available.

KRM means the Kubernetes Resource Model: apiVersion, kind, metadata and kind-specific fields such as spec, status or ConfigMap data. Custom resources follow the same conventions. See the frontend glossary and alternatives for context.

Package Purpose
github.com/ConfigButler/krm-stream/gateway Dependency-free Go stream gateway and SSE handler
github.com/ConfigButler/krm-stream/gateway/kube Optional client-go backend and SubjectAccessReview authorizer
@configbutler/krm-stream Dependency-free ESM native and gateway connectors, and the resource/editor store
spec/v1.md and conformance Shared normative contract and executable fixtures

Guides

Requirements and development

The project is pre-1.0; protocol and API changes may still be made before 1.0. Go 1.27.1 and Node 24 are required for development. Kubernetes 1.35+ supports strict resource-version ordering; OrderingLenient accommodates known non-conformant or aggregated APIs with a reduced per-object monotonicity guarantee.

task fixtures-check
task test
task lint
task build-client

See CONTRIBUTING.md. Licensed under Apache-2.0.

About

Live Kubernetes resource updates for browser apps, includes optional three-way merge lib for handling local edits

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages